@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.
Files changed (58) hide show
  1. package/.fleet/run.json +11 -0
  2. package/lib/ApiCertificate.d.ts +2 -2
  3. package/lib/ApiCertificate.js +4 -5
  4. package/lib/CognitoApiGatewayAuthorizer.d.ts +19 -0
  5. package/lib/CognitoApiGatewayAuthorizer.js +24 -0
  6. package/lib/DeployStack.d.ts +20 -0
  7. package/lib/DeployStack.js +48 -0
  8. package/lib/DynamoDbTable.d.ts +12 -0
  9. package/lib/DynamoDbTable.js +34 -0
  10. package/lib/FunctionIntegration.d.ts +21 -0
  11. package/lib/FunctionIntegration.js +61 -0
  12. package/lib/ICorsConfig.d.ts +5 -0
  13. package/lib/ICorsConfig.js +2 -0
  14. package/lib/NodeJsLambda.d.ts +13 -0
  15. package/lib/NodeJsLambda.js +28 -0
  16. package/lib/Output.d.ts +5 -0
  17. package/lib/Output.js +13 -0
  18. package/lib/RestApi.d.ts +28 -0
  19. package/lib/RestApi.js +142 -0
  20. package/lib/Role.d.ts +2 -2
  21. package/lib/Role.js +2 -5
  22. package/lib/SsmParameter.d.ts +18 -0
  23. package/lib/SsmParameter.js +40 -0
  24. package/lib/StaticWebSite.d.ts +12 -0
  25. package/lib/StaticWebSite.js +107 -0
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +15 -3
  28. package/lib/methods/apply-honeycomb-to-lambda.d.ts +3 -0
  29. package/lib/methods/apply-honeycomb-to-lambda.js +22 -0
  30. package/lib/methods/build-node-source.d.ts +13 -0
  31. package/lib/methods/build-node-source.js +105 -0
  32. package/lib/methods/duration.d.ts +8 -0
  33. package/lib/methods/duration.js +22 -0
  34. package/lib/methods/empty-directory.d.ts +2 -0
  35. package/lib/methods/empty-directory.js +47 -0
  36. package/lib/methods/generate-hash.d.ts +15 -0
  37. package/lib/methods/generate-hash.js +59 -0
  38. package/lib/methods/get-source-directory.d.ts +4 -0
  39. package/lib/methods/get-source-directory.js +37 -0
  40. package/lib/methods/walk-directory.d.ts +14 -0
  41. package/lib/methods/walk-directory.js +48 -0
  42. package/package.json +1 -1
  43. package/src/CognitoApiGatewayAuthorizer.ts +25 -0
  44. package/src/DynamoDbTable.ts +40 -0
  45. package/src/FunctionIntegration.ts +83 -0
  46. package/src/ICorsConfig.ts +5 -0
  47. package/src/NodeJsLambda.ts +31 -0
  48. package/src/NodeJsLambdaLayer.ts +27 -0
  49. package/src/RestApi.ts +178 -0
  50. package/src/SsmParameter.ts +47 -0
  51. package/src/index.ts +9 -3
  52. package/src/methods/apply-honeycomb-to-lambda.ts +22 -0
  53. package/src/methods/build-node-source.ts +97 -0
  54. package/src/methods/duration.ts +24 -0
  55. package/src/methods/empty-directory.ts +19 -0
  56. package/src/methods/generate-hash.ts +49 -0
  57. package/src/methods/get-source-directory.ts +11 -0
  58. 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,5 @@
1
+ export interface ICorsConfig {
2
+ maxAge?: number
3
+ exposeHeaders?: string[],
4
+ allowOrigin?: string|string[]
5
+ }
@@ -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 {IDomainConfig} from "./IDomainConfig";
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 {Role} from "./Role";
7
- export {StaticWebSite} from "./StaticWebSite";
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
+ }