@raisenow/tamaro-cli 1.0.8 → 1.0.9

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/src/cli.ts CHANGED
@@ -2,10 +2,18 @@
2
2
 
3
3
  import {createCommand} from 'commander'
4
4
  import pkg from '../package.json'
5
+ import {logError} from 'lib/helpers'
5
6
  import {dev, DevOptions} from 'commands/dev'
6
7
  import {build, BuildOptions} from 'commands/build'
7
- import {deploy, DeployOptions} from 'commands/deploy'
8
+ import {DEFAULT_TAG, deploy, DeployOptions} from 'commands/deploy'
8
9
  import {serve} from 'commands/serve'
10
+ import {listDeployed, ListDeployedOptions} from 'commands/list-deployed'
11
+ import {
12
+ DEFAULT_AWS_S3_EMAIL_CONFIG_BUCKET,
13
+ deployEmailConfig,
14
+ DeployEmailConfigOptions,
15
+ } from 'commands/deploy-email-config'
16
+ import {DEFAULT_AWS_PROFILE} from 'lib/aws'
9
17
 
10
18
  ///////////////////////////////////////////////////////////////////////////////
11
19
 
@@ -18,7 +26,7 @@ process.on('unhandledRejection', (error) => {
18
26
 
19
27
  // Exit on ctrl+c
20
28
  process.on('SIGINT', () => {
21
- process.exit(0)
29
+ process.exit(1)
22
30
  })
23
31
 
24
32
  ///////////////////////////////////////////////////////////////////////////////
@@ -29,8 +37,14 @@ const cli = createCommand().name(pkg.name).version(pkg.version, '-v, --version')
29
37
 
30
38
  cli
31
39
  .command('dev')
32
- .description('Run dev web-server for Tamaro Core or customer configuration')
33
- .option('--local-core', 'Load Tamaro Core from localhost instead of CDN')
40
+ .description(
41
+ 'Run the local development server for Tamaro Core or a particular customer configuration',
42
+ )
43
+ .option(
44
+ '--local-core',
45
+ 'Load Tamaro Core from localhost instead of the CDN',
46
+ false,
47
+ )
34
48
  .action(async (options: DevOptions) => {
35
49
  await dev(options)
36
50
  })
@@ -38,17 +52,31 @@ cli
38
52
  cli
39
53
  .command('build')
40
54
  .description(
41
- 'Build optimised (minified) bundle of Tamaro Core or customer configuration for production',
55
+ 'Build an optimised (minified) bundle of Tamaro Core or a customer configuration',
42
56
  )
43
- .option('--local-core', 'Load Tamaro Core from localhost instead of CDN')
44
- .option('--analyze', 'Generate bundle statistics to "reports" folder')
57
+ .option(
58
+ '--local-core',
59
+ 'Load Tamaro Core from localhost instead of the CDN',
60
+ false,
61
+ )
62
+ .option('--analyze', 'Generate bundle statistics to "reports" folder', false)
45
63
  .option(
46
64
  '--deploy',
47
- 'Deploy Tamaro Core or customer configuration bundle to AWS',
65
+ 'Deploy Tamaro Core or a customer configuration bundle to AWS S3',
66
+ false,
67
+ )
68
+ .option('--serve', 'Run the generated bundle with a local web server', false)
69
+ .option(
70
+ '--tag <tag>',
71
+ 'Tag which should be used for the deployment of the bundle',
72
+ DEFAULT_TAG,
48
73
  )
49
- .option('--serve', 'Run web-server for the bundle')
50
- .option('--tag <tag>', 'Tag which will be used for deployment of the bundle')
51
- .action(async (options: BuildOptions) => {
74
+ .option(
75
+ '--profile <profile>',
76
+ 'AWS profile which should be used for the deployment of the bundle',
77
+ DEFAULT_AWS_PROFILE,
78
+ )
79
+ .action(async (options: BuildOptions & DeployOptions) => {
52
80
  await build(options)
53
81
 
54
82
  if (options.deploy) {
@@ -63,21 +91,71 @@ cli
63
91
  cli
64
92
  .command('deploy')
65
93
  .description(
66
- 'Deploy prebuilt Tamaro Core or customer configuration bundle to AWS',
94
+ 'Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3',
95
+ )
96
+ .option(
97
+ '--tag <tag>',
98
+ 'Tag which should be used for the deployment of the bundle',
99
+ DEFAULT_TAG,
100
+ )
101
+ .option(
102
+ '--profile <profile>',
103
+ 'AWS profile which should be used for the deployment of the bundle',
104
+ DEFAULT_AWS_PROFILE,
67
105
  )
68
- .option('--tag <tag>', 'Tag which will be used for deployment of the bundle')
69
106
  .action(async (options: DeployOptions) => {
70
107
  await deploy(options)
71
108
  })
72
109
 
73
110
  cli
74
111
  .command('serve')
75
- .description('Run web-server for prebuilt bundle')
112
+ .description('Run a local web server for pre-built bundle')
76
113
  .action(async () => {
77
114
  await serve()
78
115
  })
79
116
 
117
+ cli
118
+ .command('list-deployed')
119
+ .description(
120
+ 'List deployments of Tamaro Core or a particular customer configuration',
121
+ )
122
+ .option(
123
+ '--config <config>',
124
+ 'Configuration name (default: current customer configuration)',
125
+ )
126
+ .option(
127
+ '--profile <profile>',
128
+ 'AWS profile which should be used for fetching deployments',
129
+ DEFAULT_AWS_PROFILE,
130
+ )
131
+ .action(async (options: ListDeployedOptions) => {
132
+ await listDeployed(options)
133
+ })
134
+
135
+ cli
136
+ .command('deploy-email-config')
137
+ .description('Deploy a widget’s email configuration to AWS S3')
138
+ .option(
139
+ '--bucket <bucket>',
140
+ 'AWS S3 bucket where it should be deployed',
141
+ DEFAULT_AWS_S3_EMAIL_CONFIG_BUCKET,
142
+ )
143
+ .option(
144
+ '--profile <profile>',
145
+ 'AWS profile which should be used for the deployment of email configuration',
146
+ DEFAULT_AWS_PROFILE,
147
+ )
148
+ .action(async (options: DeployEmailConfigOptions) => {
149
+ await deployEmailConfig(options)
150
+ })
151
+
80
152
  ///////////////////////////////////////////////////////////////////////////////
81
153
  ;(async () => {
82
- await cli.parseAsync(process.argv)
154
+ try {
155
+ await cli.parseAsync(process.argv)
156
+ } catch (error: any) {
157
+ if (error.signal === 'SIGINT' && error.signalDescription) {
158
+ logError(`\n${error.signalDescription}`)
159
+ }
160
+ }
83
161
  })()
@@ -0,0 +1,97 @@
1
+ import resolveBin from 'resolve-bin'
2
+ import {
3
+ fail,
4
+ getIfCoreFns,
5
+ getPaths,
6
+ getWidgetUuid,
7
+ logCommand,
8
+ logDataTable,
9
+ logTitle,
10
+ notify,
11
+ resolveOwn,
12
+ runCommandSync,
13
+ } from 'lib/helpers'
14
+ import {assertProfileValid} from 'lib/aws'
15
+ import {CORE_CONFIG_NAME, DeployOptions} from 'commands/deploy'
16
+
17
+ ///////////////////////////////////////////////////////////////////////////////
18
+
19
+ export type BuildOptions = {
20
+ localCore: boolean
21
+ analyze: boolean
22
+ deploy: boolean
23
+ serve: boolean
24
+ }
25
+
26
+ ///////////////////////////////////////////////////////////////////////////////
27
+
28
+ export const build = async (
29
+ options: BuildOptions & DeployOptions,
30
+ ): Promise<void> => {
31
+ assertOptionsValid(options)
32
+
33
+ const flags = prepareFlags(options)
34
+ const {ifCore} = getIfCoreFns()
35
+ const paths = getPaths(ifCore)
36
+ const configName = ifCore(CORE_CONFIG_NAME, getWidgetUuid())
37
+ const title = ifCore(
38
+ 'Building optimised (minified) bundle of Tamaro Core …',
39
+ `Building optimised (minified) bundle of “${configName}” customer configuration …`,
40
+ )
41
+
42
+ const wpBin = resolveBin.sync('webpack')
43
+ const wpConfig = resolveOwn('dist/webpack.config.js')
44
+ const cmd = `
45
+ ${wpBin}
46
+ --config ${wpConfig}
47
+ --env min
48
+ ${flags}
49
+ `
50
+
51
+ logTitle(title)
52
+ logCommand(cmd)
53
+ runCommandSync(cmd, {stdio: 'inherit'})
54
+
55
+ /////////////////////////////////////////////////////////////////////////////
56
+
57
+ logDataTable(
58
+ {'Path:': paths.appDist},
59
+ `Bundle for “${configName}” customer configuration is done.`,
60
+ )
61
+
62
+ /////////////////////////////////////////////////////////////////////////////
63
+
64
+ notify({
65
+ title: 'build',
66
+ message: `Bundle for “${configName}” customer configuration is done.`,
67
+ })
68
+ }
69
+
70
+ ///////////////////////////////////////////////////////////////////////////////
71
+
72
+ const assertOptionsValid = (options: BuildOptions & DeployOptions) => {
73
+ const {localCore, deploy, profile} = options
74
+ const {ifCore} = getIfCoreFns()
75
+
76
+ if (ifCore() && localCore) {
77
+ fail('Flag "--local-core" is redundant if running in Tamaro Core context.')
78
+ }
79
+
80
+ if (localCore && deploy) {
81
+ fail('Flags "--local-core" and "--deploy" must not be used together.')
82
+ }
83
+
84
+ assertProfileValid(profile)
85
+ }
86
+
87
+ ///////////////////////////////////////////////////////////////////////////////
88
+
89
+ const prepareFlags = (options: BuildOptions): string => {
90
+ const {localCore, analyze} = options
91
+ let flags: string[] = []
92
+
93
+ flags = localCore ? [...flags, '--env localCore'] : flags
94
+ flags = analyze ? [...flags, '--env analyze'] : flags
95
+
96
+ return flags.join(' ')
97
+ }
@@ -0,0 +1,107 @@
1
+ import {existsSync} from 'fs'
2
+ import {
3
+ fail,
4
+ getIfCoreFns,
5
+ getPaths,
6
+ getWidgetUuid,
7
+ logCommand,
8
+ logDataTable,
9
+ logTitle,
10
+ notify,
11
+ } from 'lib/helpers'
12
+ import {assertProfileValid, AwsOptions, runAwsCommandSync} from 'lib/aws'
13
+
14
+ ///////////////////////////////////////////////////////////////////////////////
15
+
16
+ export type DeployEmailConfigOptions = AwsOptions & {
17
+ bucket: string
18
+ }
19
+
20
+ ///////////////////////////////////////////////////////////////////////////////
21
+
22
+ export const DEFAULT_AWS_S3_EMAIL_CONFIG_BUCKET = 'rnw-email-service'
23
+
24
+ ///////////////////////////////////////////////////////////////////////////////
25
+
26
+ export const deployEmailConfig = async (
27
+ options: DeployEmailConfigOptions,
28
+ ): Promise<void> => {
29
+ const {ifCore} = getIfCoreFns()
30
+ const paths = getPaths(ifCore)
31
+ const emailConfigPath = `${paths.app}/email-config`
32
+
33
+ assertOptionsValid(options)
34
+ assertEmailConfigPathExists(emailConfigPath)
35
+ assertIsNotCore()
36
+
37
+ const {bucket} = options
38
+ const configName = getWidgetUuid()
39
+ const deployUrl = `s3://${bucket}/${configName}`
40
+
41
+ const cmd = `
42
+ aws s3 sync ${emailConfigPath} ${deployUrl}
43
+ --delete
44
+ --exact-timestamps
45
+ `
46
+ logTitle(
47
+ `Deploying email configuration for “${configName}” customer configuration to AWS S3 …`,
48
+ )
49
+ logCommand(cmd)
50
+
51
+ try {
52
+ runAwsCommandSync(cmd, {stdout: 'inherit'})
53
+ } catch (error: any) {
54
+ fail(error.stderr)
55
+ }
56
+
57
+ /////////////////////////////////////////////////////////////////////////////
58
+
59
+ logDataTable(
60
+ {'Deploy URL:': deployUrl},
61
+ `Email configuration for “${configName}” customer configuration is deployed.`,
62
+ )
63
+
64
+ /////////////////////////////////////////////////////////////////////////////
65
+
66
+ notify({
67
+ title: `deploy-email-config`,
68
+ message: `Email configuration for “${configName}” customer configuration is deployed.`,
69
+ })
70
+ }
71
+
72
+ ///////////////////////////////////////////////////////////////////////////////
73
+
74
+ const assertOptionsValid = (options: DeployEmailConfigOptions) => {
75
+ const {bucket, profile} = options
76
+
77
+ assertBucketValid(bucket)
78
+ assertProfileValid(profile)
79
+ }
80
+
81
+ ///////////////////////////////////////////////////////////////////////////////
82
+
83
+ const assertEmailConfigPathExists = (distPath: string) => {
84
+ if (!existsSync(distPath)) {
85
+ fail('Email config folder does not exists. Nothing to deploy.')
86
+ }
87
+ }
88
+
89
+ ///////////////////////////////////////////////////////////////////////////////
90
+
91
+ const assertBucketValid = (tag: string) => {
92
+ const regex = /^[a-zA-Z0-9-_]+$/
93
+
94
+ if (!regex.test(tag)) {
95
+ fail(`Flag "--bucket" has forbidden format. Allowed format: ${regex}.`)
96
+ }
97
+ }
98
+
99
+ ///////////////////////////////////////////////////////////////////////////////
100
+
101
+ const assertIsNotCore = () => {
102
+ const {ifCore} = getIfCoreFns()
103
+
104
+ if (ifCore()) {
105
+ fail('You cannot deploy widget email configuration for Tamaro Core.')
106
+ }
107
+ }
@@ -0,0 +1,151 @@
1
+ import {existsSync} from 'fs'
2
+ import stripIndent from 'strip-indent'
3
+ import {
4
+ fail,
5
+ getIfCoreFns,
6
+ getPaths,
7
+ getWidgetUuid,
8
+ logCommand,
9
+ logDataTable,
10
+ logTitle,
11
+ notify,
12
+ } from 'lib/helpers'
13
+ import {assertProfileValid, AwsOptions, runAwsCommandSync} from 'lib/aws'
14
+
15
+ ///////////////////////////////////////////////////////////////////////////////
16
+
17
+ export type DeployOptions = AwsOptions & {
18
+ tag: string
19
+ }
20
+
21
+ ///////////////////////////////////////////////////////////////////////////////
22
+
23
+ export const AWS_S3_BUCKET = 'tamaro.raisenow.com'
24
+ export const CORE_CONFIG_NAME = 'tamaro-core'
25
+ const AWS_CLOUDFRONT_DISTRIBUTION_ID = 'EHJ1OM458YQ0I'
26
+ export const DEFAULT_TAG = 'latest'
27
+
28
+ ///////////////////////////////////////////////////////////////////////////////
29
+
30
+ export const deploy = async (options: DeployOptions): Promise<void> => {
31
+ const {ifCore} = getIfCoreFns()
32
+ const paths = getPaths(ifCore)
33
+
34
+ assertOptionsValid(options)
35
+ assertDistPathExists(paths.appDist)
36
+
37
+ const {tag} = options
38
+ const configName = ifCore(CORE_CONFIG_NAME, getWidgetUuid())
39
+ const deployUrl = `s3://${AWS_S3_BUCKET}/${configName}/${tag}`
40
+ const entryFilename = ifCore('index.js', 'widget.js')
41
+
42
+ /////////////////////////////////////////////////////////////////////////////
43
+ // Sync all files except entrypoint (without deleting old files)
44
+ /////////////////////////////////////////////////////////////////////////////
45
+
46
+ const cmdSyncAll = `
47
+ aws s3 sync ${paths.appDist} ${deployUrl}
48
+ --acl public-read
49
+ --size-only
50
+ --exclude ${paths.appDist}/${entryFilename}
51
+ --cache-control max-age=31536000
52
+ `
53
+ logTitle('Deploying to AWS S3 …')
54
+ logCommand(cmdSyncAll)
55
+
56
+ try {
57
+ runAwsCommandSync(cmdSyncAll, {stdout: 'inherit'})
58
+ } catch (error: any) {
59
+ fail(error.stderr)
60
+ }
61
+
62
+ /////////////////////////////////////////////////////////////////////////////
63
+ // Copy entrypoint
64
+ /////////////////////////////////////////////////////////////////////////////
65
+
66
+ const cmdCpEntry = `
67
+ aws s3 cp ${paths.appDist}/${entryFilename} ${deployUrl}/${entryFilename}
68
+ --acl public-read
69
+ --cache-control max-age=64800
70
+ `
71
+ logCommand(cmdCpEntry)
72
+
73
+ try {
74
+ runAwsCommandSync(cmdCpEntry, {stdout: 'inherit'})
75
+ } catch (error: any) {
76
+ fail(error.stderr)
77
+ }
78
+
79
+ /////////////////////////////////////////////////////////////////////////////
80
+ // Invalidate cloudfront cache
81
+ /////////////////////////////////////////////////////////////////////////////
82
+
83
+ const cmdInvalidateCache = `
84
+ aws cloudfront create-invalidation
85
+ --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
86
+ --paths /${configName}/${tag}/*
87
+ `
88
+ logTitle('Invalidating edge cache …')
89
+ logCommand(cmdInvalidateCache)
90
+
91
+ try {
92
+ // ignore output
93
+ runAwsCommandSync(cmdInvalidateCache)
94
+ } catch (error: any) {
95
+ fail(error.stderr)
96
+ }
97
+
98
+ /////////////////////////////////////////////////////////////////////////////
99
+
100
+ const demoPage = `https://${AWS_S3_BUCKET}/${configName}/${tag}/index.html`
101
+ const entryPoint = `https://${AWS_S3_BUCKET}/${configName}/${tag}/${entryFilename}`
102
+
103
+ logDataTable(
104
+ {
105
+ 'Demo page:': demoPage,
106
+ 'Entry point:': entryPoint,
107
+ },
108
+ `Bundle for “${configName}” is deployed with tag “${tag}”`,
109
+ )
110
+
111
+ /////////////////////////////////////////////////////////////////////////////
112
+
113
+ notify({
114
+ title: 'deploy',
115
+ message: stripIndent(`
116
+ Bundle for “${configName}” is deployed with tag “${tag}”.
117
+ Demo page: ${demoPage}
118
+ `),
119
+ target: demoPage,
120
+ })
121
+ }
122
+
123
+ ///////////////////////////////////////////////////////////////////////////////
124
+
125
+ const assertOptionsValid = (options: DeployOptions) => {
126
+ const {tag, profile} = options
127
+
128
+ assertTagValid(tag)
129
+ assertProfileValid(profile)
130
+ }
131
+
132
+ ///////////////////////////////////////////////////////////////////////////////
133
+
134
+ const assertDistPathExists = (distPath: string) => {
135
+ if (!existsSync(distPath)) {
136
+ fail(`
137
+ Dist folder does not exists.
138
+ Make sure you have built the bundle before trying to deploy it.
139
+ `)
140
+ }
141
+ }
142
+
143
+ ///////////////////////////////////////////////////////////////////////////////
144
+
145
+ const assertTagValid = (tag: string) => {
146
+ const regex = /^[a-zA-Z0-9-_]+$/
147
+
148
+ if (!regex.test(tag)) {
149
+ fail(`Flag "--tag" has forbidden format. Allowed format: ${regex}.`)
150
+ }
151
+ }
@@ -0,0 +1,61 @@
1
+ import resolveBin from 'resolve-bin'
2
+ import {
3
+ fail,
4
+ getIfCoreFns,
5
+ logCommand,
6
+ logTitle,
7
+ resolveOwn,
8
+ runCommandSync,
9
+ } from 'lib/helpers'
10
+
11
+ ///////////////////////////////////////////////////////////////////////////////
12
+
13
+ export type DevOptions = {
14
+ localCore: boolean
15
+ }
16
+
17
+ ///////////////////////////////////////////////////////////////////////////////
18
+
19
+ export const dev = async (options: DevOptions): Promise<void> => {
20
+ assertOptionsValid(options)
21
+
22
+ const flags = prepareFlags(options)
23
+ const {ifCore} = getIfCoreFns()
24
+ const title = ifCore(
25
+ 'Running dev web-server for Tamaro Core …',
26
+ 'Running dev web-server for customer configuration …',
27
+ )
28
+ const wpBin = resolveBin.sync('webpack')
29
+ const wpConfig = resolveOwn('dist/webpack.config.js')
30
+ const cmd = `
31
+ ${wpBin} serve
32
+ --config ${wpConfig}
33
+ ${flags}
34
+ `
35
+
36
+ logTitle(title)
37
+ logCommand(cmd)
38
+ runCommandSync(cmd, {stdio: 'inherit'})
39
+ }
40
+
41
+ ///////////////////////////////////////////////////////////////////////////////
42
+
43
+ const assertOptionsValid = (options: DevOptions) => {
44
+ const {localCore} = options
45
+ const {ifCore} = getIfCoreFns()
46
+
47
+ if (ifCore() && localCore) {
48
+ fail('Flag "--local-core" is redundant if running in Tamaro Core context.')
49
+ }
50
+ }
51
+
52
+ ///////////////////////////////////////////////////////////////////////////////
53
+
54
+ const prepareFlags = (options: DevOptions): string => {
55
+ const {localCore} = options
56
+ let flags: string[] = []
57
+
58
+ flags = localCore ? [...flags, '--env localCore'] : flags
59
+
60
+ return flags.join(' ')
61
+ }