@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 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
- * To be a one-stop shop for:
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
- * Must be able to install via [NPM](https://www.npmjs.com/) or internal registry
15
+ - Scaffolding a new project
16
+ - A dev server to allow easy brand development
17
+ - Builds distributable assets
22
18
 
23
- * Must work cross-platform (Windows and *nix)
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
- * Easy to learn commands from a inbuilt help screen
27
+ - Easy to learn commands from a inbuilt help screen
30
28
 
31
- * Easy to read/understand error messages
29
+ - Easy to read/understand error messages
32
30
 
33
- * Empathy in the error messages – can some helpful context be provided with the error to assist the user remedy the issue?
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
- * [Hapi](https://hapi.dev/) is used for the local dev server
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
- * 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.
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> | Which site we want to display, jobseeker site or recruiter site. Values are either `js` or `rs`.
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: "ff6102ff-0f4b-43d1-a2c7-83b835b8dee5",
83
- entry: "./src/index.js",
84
- assumeSite: "js",
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
- * [Dev server](#dev-server)
90
+ - [Dev server](#dev-server)
95
91
 
96
- * [Scaffold](#scaffold)
92
+ - [Scaffold](#scaffold)
97
93
 
98
- * [Build](#build)
94
+ - [Build](#build)
99
95
 
100
- * [Publish](#publish)
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>` | Specify the clientPropertyId to use in the new project |
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
@@ -47,6 +47,8 @@ const run = () => {
47
47
  )
48
48
  .action((...args) => require('./commands/init')(...args));
49
49
 
50
+ cli.option('--no-cache', 'Do not use cache');
51
+ cli.option('--purge-cache', 'Purge all caches');
50
52
  cli.help();
51
53
  cli.version(VERSION);
52
54
  cli.parse();
@@ -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:\n${JSON.stringify(
103
- paths
104
- )}\n\nNote this may take upto ${chalk.yellow(
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)
@@ -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
- INVALIDATION_PATH,
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 invalidationPath = Hoek.reachTemplate(templateCtx, INVALIDATION_PATH);
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 result = await assetStore.uploadDir(localDir);
72
+ const uploadResult = await assetStore.uploadDir(localDir);
56
73
 
57
- log.success(`Publish complete in ${(result.duration / 1000).toFixed(0)}s\n`);
74
+ log.success(
75
+ `Publish complete in ${(uploadResult.duration / 1000).toFixed(0)}s\n`
76
+ );
58
77
 
59
- await assetStore.invalidatePaths([invalidationPath]);
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
  };
@@ -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 { slug } = require('../utils/slug');
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 flatCache = require('flat-cache');
10
- const { diskCacheWithTtl } = require('./disk-cache-with-ttl');
5
+ const { persistentCacheWithTtl } = require('./persistent-cache-with-ttl');
11
6
 
12
- dayjs.extend(duration);
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) {
@@ -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
- module.exports = function resolveExternalAssets(assets = {}) {
58
+ function resolveExternalAssets(assets = {}) {
59
59
  const result = {
60
60
  links: [],
61
61
  scripts: [],
62
62
  };
63
63
 
64
- const links = Array.isArray(assets.links) ? assets.links : [assets.links];
65
- links.forEach((item) => {
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
- const scripts = Array.isArray(assets.scripts)
70
- ? assets.scripts
71
- : [assets.scripts];
72
- scripts.forEach((item) => {
73
- result.scripts.push(new ScriptAsset(item));
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
- INVALIDATION_PATH: `/{fertConfig.client.rootClientPropertyId}/jobseekers-frontend/{fertConfig.clientPropertyId}/*`,
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.5",
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-ssm": "^3.453.0",
27
- "@aws-sdk/credential-providers": "^3.451.0",
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;