@madgex/fert 2.0.5 → 2.1.0
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/README.md +35 -33
- package/bin/cli.js +2 -0
- package/bin/commands/init.js +1 -1
- package/bin/commands/publish-tasks/asset-store-uploader.js +9 -6
- package/bin/commands/publish.js +35 -5
- package/bin/utils/cpid-lookup.js +13 -17
- package/bin/utils/index.js +25 -2
- package/bin/utils/lookup-cf-distribution-ids.js +84 -0
- package/bin/utils/persistent-cache-with-ttl.js +49 -0
- package/bin/utils/persistent-cache-with-ttl.test.js +42 -0
- package/bin/utils/resolve-external-assets.js +23 -11
- package/bin/utils/resolve-external-assets.test.js +98 -0
- package/constants.js +13 -1
- package/package.json +4 -3
- package/bin/utils/disk-cache-with-ttl.js +0 -29
package/README.md
CHANGED
|
@@ -10,45 +10,41 @@ The Madgex job board brands all have a custom headers, footers and their associa
|
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Scaffolding a new project
|
|
16
|
-
|
|
17
|
-
* A dev server to allow easy brand development
|
|
18
|
-
|
|
19
|
-
* Builds distributable assets
|
|
13
|
+
- To be a one-stop shop for:
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
- Scaffolding a new project
|
|
16
|
+
- A dev server to allow easy brand development
|
|
17
|
+
- Builds distributable assets
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
- Must be able to install via [NPM](https://www.npmjs.com/) or internal registry
|
|
20
|
+
|
|
21
|
+
- Must work cross-platform (Windows and \*nix)
|
|
24
22
|
|
|
25
23
|
## A human-first design
|
|
26
24
|
|
|
27
25
|
To empower & serve its users, we should design this CLI with the users in mind. Ensuring it feels as familiar as possible, following established patterns and is has the following:
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
- Easy to learn commands from a inbuilt help screen
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
- Easy to read/understand error messages
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
- Empathy in the error messages – can some helpful context be provided with the error to assist the user remedy the issue?
|
|
34
32
|
|
|
35
33
|
# Under the hood
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* [Vite](https://vitejs.dev/) for HMR, serves the JS entry & bundles assets for production
|
|
40
|
-
|
|
41
|
-
* [Style Dictionary](https://amzn.github.io/style-dictionary/#/README) is used to transform Design Tokens into various outputs, inc. JSON, CSS Custom Properties and SCSS variables. The output is built into `public/tokens/`
|
|
35
|
+
- [Hapi](https://hapi.dev/) is used for the local dev server
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
- [Vite](https://vitejs.dev/) for HMR, serves the JS entry & bundles assets for production
|
|
44
38
|
|
|
39
|
+
- [Style Dictionary](https://amzn.github.io/style-dictionary/#/README) is used to transform Design Tokens into various outputs, inc. JSON, CSS Custom Properties and SCSS variables. The output is built into `public/tokens/`
|
|
45
40
|
|
|
41
|
+
- Chokidar is used to watch for changes in the `templates` and `public/tokens` directories and either refresh the browser page or build a new set of tokens respectively.
|
|
46
42
|
|
|
47
43
|
# Configuration
|
|
48
44
|
|
|
49
45
|
There are **three required files** in the root of the branding repo:
|
|
50
46
|
|
|
51
|
-
`brand.json` [required] – a JSON file that supplies the base branding tokens. These values will be merged with the Design System tokens & a full set of branding tokens will be created.
|
|
47
|
+
`brand.json` [required] – a JSON file that supplies the base branding tokens. These values will be merged with the Design System tokens & a full set of branding tokens will be created. [see here for format](https://github.com/wiley/madgex-design-system/tree/master/packages/%40madgex.design-system/src/tokens)
|
|
52
48
|
|
|
53
49
|
e.g.
|
|
54
50
|
|
|
@@ -60,7 +56,7 @@ e.g.
|
|
|
60
56
|
"base": { "value": "#005689" }
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
|
-
}
|
|
59
|
+
}
|
|
64
60
|
// more branding tokens applied here...
|
|
65
61
|
}
|
|
66
62
|
```
|
|
@@ -71,7 +67,7 @@ e.g.
|
|
|
71
67
|
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
72
68
|
| `clientPropertyId` <String> | The `clientPropertyId` associated with the branding repo (required) |
|
|
73
69
|
| `entry` <String> | The JavaScript entry file. Any assets (CSS/SCSS/SVGs) included will be processed and be part of the output. Defaults to `['src/index.js']` |
|
|
74
|
-
| `assumeSite` <String>
|
|
70
|
+
| `assumeSite` <String> | Which site we want to display, jobseeker site or recruiter site. Values are either `js` or `rs`. |
|
|
75
71
|
|
|
76
72
|
The `entry` will be used by Vite to create production bundles.
|
|
77
73
|
|
|
@@ -79,9 +75,9 @@ e.g.
|
|
|
79
75
|
|
|
80
76
|
```js
|
|
81
77
|
module.exports = {
|
|
82
|
-
clientPropertyId:
|
|
83
|
-
entry:
|
|
84
|
-
assumeSite:
|
|
78
|
+
clientPropertyId: 'ff6102ff-0f4b-43d1-a2c7-83b835b8dee5',
|
|
79
|
+
entry: './src/index.js',
|
|
80
|
+
assumeSite: 'js',
|
|
85
81
|
};
|
|
86
82
|
```
|
|
87
83
|
|
|
@@ -91,13 +87,13 @@ module.exports = {
|
|
|
91
87
|
|
|
92
88
|
The CLI will support 3 main commands: dev server (default command), scaffolding a new project and building production assets.
|
|
93
89
|
|
|
94
|
-
|
|
90
|
+
- [Dev server](#dev-server)
|
|
95
91
|
|
|
96
|
-
|
|
92
|
+
- [Scaffold](#scaffold)
|
|
97
93
|
|
|
98
|
-
|
|
94
|
+
- [Build](#build)
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
- [Publish](#publish)
|
|
101
97
|
|
|
102
98
|
## Dev server
|
|
103
99
|
|
|
@@ -133,10 +129,10 @@ When faced with an empty branding repo, this will help the user get started with
|
|
|
133
129
|
|
|
134
130
|
**Options**
|
|
135
131
|
|
|
136
|
-
| Option
|
|
137
|
-
|
|
|
138
|
-
| `--template <template>` | Specify the template you wish to use when scaffolding
|
|
139
|
-
| `--cpid <cpid>`
|
|
132
|
+
| Option | |
|
|
133
|
+
| ----------------------- | ------------------------------------------------------ |
|
|
134
|
+
| `--template <template>` | Specify the template you wish to use when scaffolding |
|
|
135
|
+
| `--cpid <cpid>` | Specify the clientPropertyId to use in the new project |
|
|
140
136
|
|
|
141
137
|
**Outline of implementation**
|
|
142
138
|
|
|
@@ -194,7 +190,13 @@ Index all the files and their paths in the `dist` directory, and using a recursi
|
|
|
194
190
|
|
|
195
191
|
The Asset Store API uses a multi-part form upload to PUT files in the client directory. Upload paths are respected, directories are created on the fly.
|
|
196
192
|
|
|
193
|
+
# Global options
|
|
194
|
+
|
|
195
|
+
These options are available no matter the command used.
|
|
196
|
+
|
|
197
|
+
`--no-cache` — Do not use the in-built file-based cache. This includes CPID, config & AWS CloudFront Distribution lookup (default: true)
|
|
197
198
|
|
|
199
|
+
`--purge-cache` — Purge/clean all local caches
|
|
198
200
|
|
|
199
201
|
# CI/CD
|
|
200
202
|
|
|
@@ -213,4 +215,4 @@ Here are the default npm scripts in a Fert-scaffolded project.
|
|
|
213
215
|
}
|
|
214
216
|
```
|
|
215
217
|
|
|
216
|
-
Its expected that a Jenkins pipeline will run `fert publish` to send all built assets to the Asset Store API.
|
|
218
|
+
Its expected that a Jenkins pipeline will run `fert publish` to send all built assets to the Asset Store API.
|
package/bin/cli.js
CHANGED
package/bin/commands/init.js
CHANGED
|
@@ -104,7 +104,7 @@ module.exports = async (root, options = {}) => {
|
|
|
104
104
|
).start();
|
|
105
105
|
let clientProperties;
|
|
106
106
|
try {
|
|
107
|
-
clientProperties = await cpidLookup(clientPropertyId);
|
|
107
|
+
clientProperties = await cpidLookup(clientPropertyId, options);
|
|
108
108
|
spinner.succeed(
|
|
109
109
|
`Looking up clientPropertyId: ${chalk.green(clientPropertyId)}`
|
|
110
110
|
);
|
|
@@ -81,8 +81,9 @@ module.exports = class AssetStoreUploader {
|
|
|
81
81
|
/**
|
|
82
82
|
* Invalidate files on all CloudFront edge locations
|
|
83
83
|
* @param {array} paths Paths to invalidate
|
|
84
|
+
* @param {string?} distributionId CloudFront distribution id to invalidate
|
|
84
85
|
*/
|
|
85
|
-
async invalidatePaths(paths) {
|
|
86
|
+
async invalidatePaths(paths, distributionId = false) {
|
|
86
87
|
const invalidateUrl = new URL('api/assets/invalidate', this.apiUrl);
|
|
87
88
|
|
|
88
89
|
log.debug('invalidating', { paths });
|
|
@@ -92,6 +93,10 @@ module.exports = class AssetStoreUploader {
|
|
|
92
93
|
paths,
|
|
93
94
|
};
|
|
94
95
|
|
|
96
|
+
if (distributionId) {
|
|
97
|
+
payload.distributionId = distributionId;
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
await axios.post(invalidateUrl.href, payload, {
|
|
96
101
|
headers: {
|
|
97
102
|
apikey: this.apiKey,
|
|
@@ -99,11 +104,9 @@ module.exports = class AssetStoreUploader {
|
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
log.info(
|
|
102
|
-
`CloudFront cache invalidation sent for the following paths
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'30 seconds'
|
|
106
|
-
)} to complete.\n`
|
|
107
|
+
`CloudFront cache invalidation sent for the following paths${
|
|
108
|
+
distributionId ? ` (distribution id: ${distributionId})` : ''
|
|
109
|
+
}:\n${JSON.stringify(paths)}\n`
|
|
107
110
|
);
|
|
108
111
|
}
|
|
109
112
|
// Returns array of all files in provided directory (recursive)
|
package/bin/commands/publish.js
CHANGED
|
@@ -5,12 +5,16 @@ const { resolveConfig } = require('../utils');
|
|
|
5
5
|
const { log } = require('../utils/logging');
|
|
6
6
|
const getAwsParam = require('./publish-tasks/get-aws-parameter');
|
|
7
7
|
const AssetStoreUploader = require('./publish-tasks/asset-store-uploader');
|
|
8
|
+
const {
|
|
9
|
+
getCloudFrontDistributionsForDomain,
|
|
10
|
+
} = require('../utils/lookup-cf-distribution-ids');
|
|
8
11
|
const {
|
|
9
12
|
ASSET_STORE_API,
|
|
10
13
|
AWS_PARAM_NAME,
|
|
11
14
|
REMOTE_UPLOAD_BASE,
|
|
12
15
|
UPLOAD_DIR,
|
|
13
|
-
|
|
16
|
+
ASSET_STORE_INVALIDATION_PATH,
|
|
17
|
+
BRANDED_SITE_INVALIDATION_PATH,
|
|
14
18
|
} = require('../../constants');
|
|
15
19
|
|
|
16
20
|
module.exports = async (root, options) => {
|
|
@@ -33,10 +37,23 @@ module.exports = async (root, options) => {
|
|
|
33
37
|
const apiUrl = Hoek.reachTemplate(templateCtx, ASSET_STORE_API);
|
|
34
38
|
const remoteBasePath = Hoek.reachTemplate(templateCtx, REMOTE_UPLOAD_BASE);
|
|
35
39
|
const apiKeyParamPath = Hoek.reachTemplate(templateCtx, AWS_PARAM_NAME);
|
|
36
|
-
const
|
|
40
|
+
const assetStoreInvPath = Hoek.reachTemplate(
|
|
41
|
+
templateCtx,
|
|
42
|
+
ASSET_STORE_INVALIDATION_PATH
|
|
43
|
+
);
|
|
44
|
+
const brandedSiteInvPath = Hoek.reachTemplate(
|
|
45
|
+
templateCtx,
|
|
46
|
+
BRANDED_SITE_INVALIDATION_PATH
|
|
47
|
+
);
|
|
37
48
|
|
|
38
49
|
const apiKey = await getAwsParam(apiKeyParamPath);
|
|
39
50
|
|
|
51
|
+
const siteUrl = new URL(fertConfig.config.JobseekerSiteWebSitePath);
|
|
52
|
+
const clientCloudFrontDists = await getCloudFrontDistributionsForDomain(
|
|
53
|
+
siteUrl.hostname,
|
|
54
|
+
options
|
|
55
|
+
);
|
|
56
|
+
|
|
40
57
|
log.info(
|
|
41
58
|
`\nPublishing ${chalk.cyan(localDir)} to ${chalk.green.bold(
|
|
42
59
|
options.target
|
|
@@ -52,9 +69,22 @@ module.exports = async (root, options) => {
|
|
|
52
69
|
basePath: remoteBasePath,
|
|
53
70
|
});
|
|
54
71
|
|
|
55
|
-
const
|
|
72
|
+
const uploadResult = await assetStore.uploadDir(localDir);
|
|
56
73
|
|
|
57
|
-
log.success(
|
|
74
|
+
log.success(
|
|
75
|
+
`Publish complete in ${(uploadResult.duration / 1000).toFixed(0)}s\n`
|
|
76
|
+
);
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
// invalidate cloudfront cache
|
|
79
|
+
await assetStore.invalidatePaths([assetStoreInvPath]);
|
|
80
|
+
|
|
81
|
+
for (let dist of clientCloudFrontDists) {
|
|
82
|
+
await assetStore.invalidatePaths([brandedSiteInvPath], dist.id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
log.info(
|
|
86
|
+
`\nNote cloudfront invalidations may take upto ${chalk.yellow(
|
|
87
|
+
'30 seconds'
|
|
88
|
+
)} to complete.\n`
|
|
89
|
+
);
|
|
60
90
|
};
|
package/bin/utils/cpid-lookup.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
1
|
const axios = require('axios');
|
|
3
2
|
const { log } = require('../utils/logging');
|
|
4
|
-
const {
|
|
5
|
-
const dayjs = require('dayjs');
|
|
6
|
-
const duration = require('dayjs/plugin/duration');
|
|
7
|
-
const { PROPERTY_ID_API, TMP_DIR, VERSION } = require('../../constants');
|
|
3
|
+
const { PROPERTY_ID_API, ONE_WEEK } = require('../../constants');
|
|
8
4
|
const chalk = require('chalk');
|
|
9
|
-
const
|
|
10
|
-
const { diskCacheWithTtl } = require('./disk-cache-with-ttl');
|
|
5
|
+
const { persistentCacheWithTtl } = require('./persistent-cache-with-ttl');
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const flatCacheDoc = flatCache.load(
|
|
15
|
-
'fert-cpid-cache',
|
|
16
|
-
path.resolve(`${TMP_DIR}/fert-cpid-cache-${slug(VERSION)}`)
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const cache = new diskCacheWithTtl(flatCacheDoc, {
|
|
20
|
-
ttl: dayjs.duration(1, 'week').asMilliseconds(),
|
|
7
|
+
const cache = new persistentCacheWithTtl('cpid-cache', {
|
|
8
|
+
ttl: ONE_WEEK,
|
|
21
9
|
});
|
|
22
10
|
|
|
23
11
|
exports.doCpidLookup = async (clientPropertyId) => {
|
|
@@ -48,7 +36,15 @@ exports.doCpidLookup = async (clientPropertyId) => {
|
|
|
48
36
|
};
|
|
49
37
|
};
|
|
50
38
|
|
|
51
|
-
exports.cpidLookup = async (clientPropertyId) => {
|
|
39
|
+
exports.cpidLookup = async (clientPropertyId, options = {}) => {
|
|
40
|
+
if (options.purgeCache) {
|
|
41
|
+
cache.purgeCacheFile();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!options.cache) {
|
|
45
|
+
return this.doCpidLookup(clientPropertyId);
|
|
46
|
+
}
|
|
47
|
+
|
|
52
48
|
const value = cache.get(clientPropertyId);
|
|
53
49
|
|
|
54
50
|
if (value) {
|
package/bin/utils/index.js
CHANGED
|
@@ -5,13 +5,31 @@ const chalk = require('chalk');
|
|
|
5
5
|
const uuidValidator = require('uuid-validate');
|
|
6
6
|
const Hoek = require('@hapi/hoek');
|
|
7
7
|
const { cpidLookup } = require('./cpid-lookup');
|
|
8
|
-
const resolveExternalAssets = require('./resolve-external-assets');
|
|
8
|
+
const { resolveExternalAssets } = require('./resolve-external-assets');
|
|
9
9
|
const { log } = require('./logging');
|
|
10
|
+
const { CONFIG_API } = require('../../constants');
|
|
11
|
+
const assert = require('node:assert');
|
|
10
12
|
|
|
11
13
|
exports.printBanner = () => {
|
|
12
14
|
console.log(`\n${chalk.green.bold('Fert')} v${VERSION}`);
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
exports.getConfig = async (cpid, configNames = []) => {
|
|
18
|
+
assert(Array.isArray(configNames), 'configNames must be an array');
|
|
19
|
+
|
|
20
|
+
const result = {};
|
|
21
|
+
|
|
22
|
+
for (let config of configNames) {
|
|
23
|
+
const url = CONFIG_API.replace('{cpid}', cpid).replace('{config}', config);
|
|
24
|
+
const { data } = await fetch(url).then((response) => response.json());
|
|
25
|
+
const { value } = data[0];
|
|
26
|
+
|
|
27
|
+
result[config] = value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
15
33
|
exports.resolveConfig = async (root, options = {}) => {
|
|
16
34
|
const defaults = {
|
|
17
35
|
clientPropertyId: null,
|
|
@@ -27,7 +45,7 @@ exports.resolveConfig = async (root, options = {}) => {
|
|
|
27
45
|
defaults,
|
|
28
46
|
this.loadConfigFromFile(workingDir)
|
|
29
47
|
);
|
|
30
|
-
const client = await cpidLookup(fertConfig.clientPropertyId);
|
|
48
|
+
const client = await cpidLookup(fertConfig.clientPropertyId, options);
|
|
31
49
|
|
|
32
50
|
Hoek.assert(
|
|
33
51
|
uuidValidator(fertConfig.clientPropertyId),
|
|
@@ -36,6 +54,11 @@ exports.resolveConfig = async (root, options = {}) => {
|
|
|
36
54
|
|
|
37
55
|
fertConfig.externalAssets = resolveExternalAssets(fertConfig.externalAssets);
|
|
38
56
|
|
|
57
|
+
fertConfig.config = await exports.getConfig(fertConfig.clientPropertyId, [
|
|
58
|
+
'JobseekerSiteWebSitePath',
|
|
59
|
+
// 'SiteName',
|
|
60
|
+
]);
|
|
61
|
+
|
|
39
62
|
fertConfig.assumeSite =
|
|
40
63
|
SITE[options.assumeSite] || SITE[fertConfig.assumeSite] || SITE.js;
|
|
41
64
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const {
|
|
2
|
+
CloudFrontClient,
|
|
3
|
+
ListDistributionsCommand,
|
|
4
|
+
} = require('@aws-sdk/client-cloudfront');
|
|
5
|
+
const { persistentCacheWithTtl } = require('./persistent-cache-with-ttl');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const { ONE_WEEK } = require('../../constants');
|
|
8
|
+
const { log } = require('../utils/logging');
|
|
9
|
+
|
|
10
|
+
const cache = new persistentCacheWithTtl('cf-distribution-cache', {
|
|
11
|
+
ttl: ONE_WEEK,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
exports.doGetAllCloudFrontDistributions = async () => {
|
|
15
|
+
const cloudfront = new CloudFrontClient();
|
|
16
|
+
const distributions = [];
|
|
17
|
+
|
|
18
|
+
let Marker = '';
|
|
19
|
+
let isTruncated = false;
|
|
20
|
+
|
|
21
|
+
do {
|
|
22
|
+
const list = await cloudfront.send(
|
|
23
|
+
new ListDistributionsCommand({ Marker })
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
Marker = list.DistributionList.NextMarker;
|
|
27
|
+
isTruncated = list.DistributionList.IsTruncated;
|
|
28
|
+
|
|
29
|
+
list.DistributionList.Items.forEach((item) => distributions.push(item));
|
|
30
|
+
} while (isTruncated);
|
|
31
|
+
|
|
32
|
+
return distributions;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
exports.getAllCloudFrontDistributions = async (options = {}) => {
|
|
36
|
+
assert(options.target, 'Target required');
|
|
37
|
+
const CACHE_KEY = `cf-distributions:${options.target}`;
|
|
38
|
+
|
|
39
|
+
if (options.purgeCache) {
|
|
40
|
+
cache.purgeCacheFile();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!options.cache) {
|
|
44
|
+
return this.doGetAllCloudFrontDistributions();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const value = cache.get(CACHE_KEY);
|
|
48
|
+
|
|
49
|
+
if (value) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log.info('\nRefreshing CloudFront Distributions cache…');
|
|
54
|
+
|
|
55
|
+
const result = await this.doGetAllCloudFrontDistributions();
|
|
56
|
+
|
|
57
|
+
cache.set(CACHE_KEY, result);
|
|
58
|
+
|
|
59
|
+
log.success('Completed refreshing CF distribution cache.\n');
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find all CloudFront distribution Ids that are associated with the supplied domain.
|
|
66
|
+
* @param {string} domain Search all CloudFront Distributions for those that match domain name.
|
|
67
|
+
* @returns {Promise<Array.<{id:string, aliases:[string]}>>} An array of objects.
|
|
68
|
+
*/
|
|
69
|
+
exports.getCloudFrontDistributionsForDomain = async (domain, options) => {
|
|
70
|
+
assert(domain && typeof domain === 'string', 'Domain name required');
|
|
71
|
+
|
|
72
|
+
const distributions = await this.getAllCloudFrontDistributions(options);
|
|
73
|
+
|
|
74
|
+
return distributions.reduce((accum, val) => {
|
|
75
|
+
const { Id, Aliases } = val;
|
|
76
|
+
const { Items = [] } = Aliases;
|
|
77
|
+
|
|
78
|
+
if (Items.find((i) => i.toLowerCase().includes(domain))) {
|
|
79
|
+
accum.push({ id: Id, aliases: Items });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return accum;
|
|
83
|
+
}, []);
|
|
84
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const flatCache = require('flat-cache');
|
|
3
|
+
const { TMP_DIR, VERSION } = require('../../constants');
|
|
4
|
+
const { slug } = require('./slug');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
|
|
7
|
+
class persistentCacheWithTtl {
|
|
8
|
+
constructor(name, options = { ttl: 0 }) {
|
|
9
|
+
assert(name, 'Cache name required');
|
|
10
|
+
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.cacheDir = path.resolve(`${TMP_DIR}/fert-${slug(VERSION)}`);
|
|
13
|
+
this.cache = this._loadCache();
|
|
14
|
+
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_loadCache() {
|
|
19
|
+
return flatCache.load(this.name, this.cacheDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set(key, value) {
|
|
23
|
+
this.cache.setKey(key, {
|
|
24
|
+
value,
|
|
25
|
+
expire: new Date().getTime() + this.options.ttl,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.cache.save(true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(key) {
|
|
32
|
+
let cachedBlob = this.cache.getKey(key);
|
|
33
|
+
|
|
34
|
+
if (cachedBlob && new Date().getTime() > cachedBlob.expire) {
|
|
35
|
+
this.cache.removeKey(key);
|
|
36
|
+
cachedBlob = null;
|
|
37
|
+
|
|
38
|
+
this.cache.save(true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (cachedBlob && cachedBlob.value) || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
purgeCacheFile() {
|
|
45
|
+
flatCache.clearCacheById(this.name, this.cacheDir);
|
|
46
|
+
this.cache = this._loadCache();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.persistentCacheWithTtl = persistentCacheWithTtl;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { persistentCacheWithTtl } = require('./persistent-cache-with-ttl');
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
|
|
6
|
+
describe('persistentCacheWithTtl', function () {
|
|
7
|
+
let cache;
|
|
8
|
+
|
|
9
|
+
beforeEach(function () {
|
|
10
|
+
cache = new persistentCacheWithTtl('test', { ttl: 5000 }); // 5 seconds ttl
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should throw an error if no name is provided', function () {
|
|
14
|
+
assert.throws(() => new persistentCacheWithTtl(), /Cache name required/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should set a value in the cache', function () {
|
|
18
|
+
cache.set('key', 'value');
|
|
19
|
+
const cachedValue = cache.get('key');
|
|
20
|
+
assert.equal(cachedValue, 'value');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return null if the key does not exist', function () {
|
|
24
|
+
const cachedValue = cache.get('nonexistent');
|
|
25
|
+
assert.equal(cachedValue, null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should remove a value from the cache after its ttl has expired', async function () {
|
|
29
|
+
cache = new persistentCacheWithTtl('test', { ttl: 1000 }); // 1 second ttl
|
|
30
|
+
cache.set('key', 'value');
|
|
31
|
+
await sleep(1500);
|
|
32
|
+
const cachedValue = cache.get('key');
|
|
33
|
+
assert.equal(cachedValue, null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should purge the cache file', function () {
|
|
37
|
+
cache.set('key', 'value');
|
|
38
|
+
cache.purgeCacheFile();
|
|
39
|
+
const cachedValue = cache.get('key');
|
|
40
|
+
assert.equal(cachedValue, null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -55,23 +55,35 @@ class ScriptAsset {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
function resolveExternalAssets(assets = {}) {
|
|
59
59
|
const result = {
|
|
60
60
|
links: [],
|
|
61
61
|
scripts: [],
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
result.links.push(new LinkAsset(item));
|
|
67
|
-
});
|
|
64
|
+
if (assets.links) {
|
|
65
|
+
const linkArr = Array.isArray(assets.links) ? assets.links : [assets.links];
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
linkArr.forEach((item) => {
|
|
68
|
+
result.links.push(new LinkAsset(item));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (assets.scripts) {
|
|
73
|
+
const scriptArr = Array.isArray(assets.scripts)
|
|
74
|
+
? assets.scripts
|
|
75
|
+
: [assets.scripts];
|
|
76
|
+
|
|
77
|
+
scriptArr.forEach((item) => {
|
|
78
|
+
result.scripts.push(new ScriptAsset(item));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
75
81
|
|
|
76
82
|
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
LinkAsset,
|
|
87
|
+
ScriptAsset,
|
|
88
|
+
resolveExternalAssets,
|
|
77
89
|
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const {
|
|
4
|
+
LinkAsset,
|
|
5
|
+
ScriptAsset,
|
|
6
|
+
resolveExternalAssets,
|
|
7
|
+
} = require('./resolve-external-assets');
|
|
8
|
+
|
|
9
|
+
describe('LinkAsset', function () {
|
|
10
|
+
it('should create a LinkAsset with href when string is passed', function () {
|
|
11
|
+
const linkAsset = new LinkAsset('http://example.com');
|
|
12
|
+
assert.strictEqual(linkAsset.href, 'http://example.com');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should create a LinkAsset with properties when object is passed', function () {
|
|
16
|
+
const linkAsset = new LinkAsset({
|
|
17
|
+
href: 'http://example.com',
|
|
18
|
+
rel: 'preload',
|
|
19
|
+
});
|
|
20
|
+
assert.strictEqual(linkAsset.href, 'http://example.com');
|
|
21
|
+
assert.strictEqual(linkAsset.rel, 'preload');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should throw error when object without href is passed', function () {
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => new LinkAsset({ rel: 'preload' }),
|
|
27
|
+
/missing the href property/
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should throw error when neither string nor object is passed', function () {
|
|
32
|
+
assert.throws(() => new LinkAsset(123), /should be a string or an object/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('ScriptAsset', function () {
|
|
37
|
+
it('should create a ScriptAsset with src when string is passed', function () {
|
|
38
|
+
const scriptAsset = new ScriptAsset('http://example.com/script.js');
|
|
39
|
+
assert.strictEqual(scriptAsset.src, 'http://example.com/script.js');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should create a ScriptAsset with properties when object is passed', function () {
|
|
43
|
+
const scriptAsset = new ScriptAsset({
|
|
44
|
+
src: 'http://example.com/script.js',
|
|
45
|
+
async: false,
|
|
46
|
+
});
|
|
47
|
+
assert.strictEqual(scriptAsset.src, 'http://example.com/script.js');
|
|
48
|
+
assert.strictEqual(scriptAsset.async, undefined);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should throw error when object without src is passed', function () {
|
|
52
|
+
assert.throws(
|
|
53
|
+
() => new ScriptAsset({ async: false }),
|
|
54
|
+
/missing the src property/
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error when neither string nor object is passed', function () {
|
|
59
|
+
assert.throws(
|
|
60
|
+
() => new ScriptAsset(123),
|
|
61
|
+
/should be a string or an object/
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('resolveExternalAssets', function () {
|
|
67
|
+
it('should resolve links and scripts from assets', function () {
|
|
68
|
+
const assets = {
|
|
69
|
+
links: [
|
|
70
|
+
'http://example.com/style.css',
|
|
71
|
+
{ href: 'http://example.com/style2.css', rel: 'preload' },
|
|
72
|
+
],
|
|
73
|
+
scripts: [
|
|
74
|
+
'http://example.com/script.js',
|
|
75
|
+
{ src: 'http://example.com/script2.js', async: false },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const result = resolveExternalAssets(assets);
|
|
79
|
+
assert.strictEqual(result.links.length, 2);
|
|
80
|
+
assert.strictEqual(result.scripts.length, 2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle single link and script in assets', function () {
|
|
84
|
+
const assets = {
|
|
85
|
+
links: 'http://example.com/style.css',
|
|
86
|
+
scripts: 'http://example.com/script.js',
|
|
87
|
+
};
|
|
88
|
+
const result = resolveExternalAssets(assets);
|
|
89
|
+
assert.strictEqual(result.links.length, 1);
|
|
90
|
+
assert.strictEqual(result.scripts.length, 1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle empty assets', function () {
|
|
94
|
+
const result = resolveExternalAssets({});
|
|
95
|
+
assert.strictEqual(result.links.length, 0);
|
|
96
|
+
assert.strictEqual(result.scripts.length, 0);
|
|
97
|
+
});
|
|
98
|
+
});
|
package/constants.js
CHANGED
|
@@ -6,6 +6,11 @@ const { version: VERSION } = JSON.parse(
|
|
|
6
6
|
fs.readFileSync(path.resolve(__dirname, './package.json')).toString()
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
+
const ONE_MINUTE = 60000;
|
|
10
|
+
const ONE_HOUR = ONE_MINUTE * 60;
|
|
11
|
+
const ONE_DAY = ONE_HOUR * 24;
|
|
12
|
+
const ONE_WEEK = ONE_DAY * 7;
|
|
13
|
+
|
|
9
14
|
module.exports = {
|
|
10
15
|
VERSION,
|
|
11
16
|
TMP_DIR: fs.realpathSync(os.tmpdir()),
|
|
@@ -23,7 +28,8 @@ module.exports = {
|
|
|
23
28
|
'/api/assets/jobseekers-frontend/{fertConfig.clientPropertyId}',
|
|
24
29
|
AWS_PARAM_NAME:
|
|
25
30
|
'/{Environment_Name}/jobboard/kong/internal/consumer-key/jobseekers-frontend_{fertConfig.client.rootClientPropertyId}',
|
|
26
|
-
|
|
31
|
+
ASSET_STORE_INVALIDATION_PATH: `/{fertConfig.client.rootClientPropertyId}/jobseekers-frontend/{fertConfig.clientPropertyId}/*`,
|
|
32
|
+
BRANDED_SITE_INVALIDATION_PATH: `/_/jobseekers-frontend/assets/*`,
|
|
27
33
|
ASSET_STORE_USER_GUID: 'a386d4b6-f2df-4b80-ad1f-0349e23f530b',
|
|
28
34
|
TEMPLATES: [
|
|
29
35
|
{
|
|
@@ -39,4 +45,10 @@ module.exports = {
|
|
|
39
45
|
js: 'jobseekers-frontend',
|
|
40
46
|
rs: 'recruiterservices-frontend',
|
|
41
47
|
},
|
|
48
|
+
ONE_MINUTE,
|
|
49
|
+
ONE_HOUR,
|
|
50
|
+
ONE_DAY,
|
|
51
|
+
ONE_WEEK,
|
|
52
|
+
CONFIG_API:
|
|
53
|
+
'https://configuration-api.job.madgexhosting.net/configs/override/{cpid}/production/clientconfig/{config}',
|
|
42
54
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madgex/fert",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Tool to help build the V6 branding",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fert": "./bin/cli.js"
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"author": "Madgex",
|
|
24
24
|
"license": "UNLICENSED",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@aws-sdk/client-
|
|
27
|
-
"@aws-sdk/
|
|
26
|
+
"@aws-sdk/client-cloudfront": "^3.515.0",
|
|
27
|
+
"@aws-sdk/client-ssm": "^3.519.0",
|
|
28
|
+
"@aws-sdk/credential-providers": "^3.515.0",
|
|
28
29
|
"@hapi/hapi": "^21.3.2",
|
|
29
30
|
"@hapi/hoek": "^11.0.2",
|
|
30
31
|
"@hapi/inert": "^7.1.0",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
class diskCacheWithTtl {
|
|
2
|
-
constructor(cache, options = { ttl: 0 }) {
|
|
3
|
-
this.cache = cache;
|
|
4
|
-
this.options = options;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
set(key, value) {
|
|
8
|
-
this.cache.setKey(key, {
|
|
9
|
-
value,
|
|
10
|
-
expire: new Date().getTime() + this.options.ttl,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
this.cache.save(true);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
get(key) {
|
|
17
|
-
let cachedBlob = this.cache.getKey(key);
|
|
18
|
-
|
|
19
|
-
if (cachedBlob && new Date().getTime() > cachedBlob.expire) {
|
|
20
|
-
this.cache.removeKey(key);
|
|
21
|
-
cachedBlob = null;
|
|
22
|
-
|
|
23
|
-
this.cache.save(true);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return cachedBlob && cachedBlob.value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
exports.diskCacheWithTtl = diskCacheWithTtl;
|