@madgex/fert 3.0.1 → 4.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/bin/cli.js CHANGED
@@ -38,6 +38,18 @@ const run = () => {
38
38
  .option('--target <env>', 'Environment to publish to, "dev" or "prod"')
39
39
  .action((...args) => require('./commands/publish')(...args));
40
40
 
41
+ cli
42
+ .command('configs [root]', 'Query/Publish project configs')
43
+ .option(
44
+ '--publish <env>',
45
+ 'Publish configs to Configuration API environment "dev" or "production"'
46
+ )
47
+ .option(
48
+ '--query [configName]',
49
+ 'Describe known keys for a config, omit to list all known configs'
50
+ )
51
+ .action((...args) => require('./commands/configs')(...args));
52
+
41
53
  cli
42
54
  .command('init [root]', 'Create a new branding project')
43
55
  .option('--template <template>', 'Template to use when scaffolding project')
@@ -63,7 +63,6 @@ module.exports = async function buildCssFromTokens(srcTokensPath, buildPath) {
63
63
  'name/cti/kebab',
64
64
  'color/css',
65
65
  'css/rawData',
66
- 'custom/pxToRem',
67
66
  ];
68
67
  const prefix = 'mds';
69
68
 
@@ -102,8 +101,7 @@ module.exports = async function buildCssFromTokens(srcTokensPath, buildPath) {
102
101
  // Use StyleDictionary to build a tokens SCSS file
103
102
  const StyleDictionary = StyleDictionaryPackage.extend(styleDictionaryConfig);
104
103
 
105
- const tokenBaseFontSize = brandObj?.font?.size?.base?.value; // will default to 16px if undefined
106
- await registerTransforms(StyleDictionary, tokenBaseFontSize);
104
+ await registerTransforms(StyleDictionary);
107
105
  await runSilently(StyleDictionary.buildAllPlatforms.bind(StyleDictionary));
108
106
 
109
107
  // clean-up temp SD file
@@ -5,9 +5,11 @@ const { log } = require('../utils/logging');
5
5
  const bundleEntry = require('./build-tasks/bundle-entry');
6
6
  const buildCssFromTokens = require('./build-tasks/build-tokens');
7
7
  const buildExternalAssets = require('./build-tasks/build-external-assets');
8
+ const { validateLocalConfigs } = require('../utils/configs.js');
8
9
 
9
10
  module.exports = async (root, options = {}) => {
10
11
  const fertConfig = await resolveConfig(root, options);
12
+ await validateLocalConfigs(fertConfig, { catch: false });
11
13
 
12
14
  if (!options.only) {
13
15
  await rimraf(path.resolve(fertConfig.workingDir, 'dist'));
@@ -0,0 +1,70 @@
1
+ const { resolveConfig } = require('../utils/index.js');
2
+ const {
3
+ getConfigAPI,
4
+ validateLocalConfigs,
5
+ updateProjectConfigs,
6
+ } = require('../utils/configs.js');
7
+ const chalk = require('chalk');
8
+ const { log } = require('../utils/logging.js');
9
+
10
+ module.exports = async (root, options) => {
11
+ const fertConfig = await resolveConfig(root, options);
12
+ const api = await getConfigAPI(fertConfig);
13
+
14
+ if (!options.publish && !options.query) {
15
+ console.log(chalk.red(`Missing required --publish or --query option`));
16
+ }
17
+
18
+ if (options.publish) {
19
+ const validTargets = ['dev', 'production'];
20
+
21
+ if (!validTargets.includes(options.publish)) {
22
+ throw Error(
23
+ `Missing or invalid --publish option. Choose from [${validTargets}]`
24
+ );
25
+ }
26
+
27
+ try {
28
+ await validateLocalConfigs(fertConfig, { catch: false });
29
+ await updateProjectConfigs(fertConfig, { environment: options.publish });
30
+ } catch (err) {
31
+ log.debug(err);
32
+ process.exit(1);
33
+ }
34
+ } else if (options.query) {
35
+ try {
36
+ if (options.query !== true) {
37
+ console.log(
38
+ chalk.cyan(`\nGetting ${options.query} schema description...`)
39
+ );
40
+ const schema = api.schemas[options.query];
41
+ if (!schema) {
42
+ console.error(chalk.red(`Schema not found for ${options.query}`));
43
+ return;
44
+ }
45
+
46
+ console.log(JSON.stringify(schema.describe().keys, null, 2));
47
+ console.log(
48
+ `\n${chalk.cyan('Defaults')}:\n${JSON.stringify(schema.validate({}), null, 2)}`
49
+ );
50
+ return;
51
+ }
52
+
53
+ console.log(chalk.cyan(`\nAvailable config schemas...`));
54
+ console.log(
55
+ Object.keys(api.schemas)
56
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
57
+ .map((key) => ` * ${key}`)
58
+ .join('\n'),
59
+ '\n'
60
+ );
61
+
62
+ console.log(
63
+ 'Use `fert configs <configName>` to get a specific schema description'
64
+ );
65
+ } catch (err) {
66
+ log.debug(err);
67
+ process.exit(1);
68
+ }
69
+ }
70
+ };
@@ -4,6 +4,7 @@ const open = require('open');
4
4
  const chokidar = require('chokidar');
5
5
  const { resolveConfig } = require('../utils/index.js');
6
6
  const { log } = require('../utils/logging.js');
7
+ const { validateLocalConfigs } = require('../utils/configs.js');
7
8
  const { devServer } = require('../../server');
8
9
  const { buildTokens, buildExternalAssets } = require('./build.js');
9
10
  const {
@@ -22,9 +23,18 @@ module.exports = async (root, options = {}) => {
22
23
  )}\n`
23
24
  );
24
25
 
26
+ await validateLocalConfigs(fertConfig);
27
+ await buildTokens(fertConfig);
28
+
29
+ // watch local configs - revalidate on change
30
+ const configsPath = path.resolve(fertConfig.workingDir, './config/*.json');
31
+ chokidar.watch(configsPath, { ignoreInitial: true }).on('all', async () => {
32
+ await validateLocalConfigs(fertConfig);
33
+ });
34
+
25
35
  // watch brand.json & fert.config.js - refresh on change
26
36
  const brandPath = path.resolve(fertConfig.workingDir, BRAND_JSON_FILENAME);
27
- chokidar.watch(brandPath).on('all', async () => {
37
+ chokidar.watch(brandPath, { ignoreInitial: true }).on('all', async () => {
28
38
  await buildTokens(fertConfig);
29
39
  });
30
40
 
@@ -5,6 +5,10 @@ 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
+ validateLocalConfigs,
10
+ updateProjectConfigs,
11
+ } = require('../utils/configs.js');
8
12
  const {
9
13
  getCloudFrontDistributionsForDomain,
10
14
  } = require('../utils/lookup-cf-distribution-ids');
@@ -62,29 +66,40 @@ module.exports = async (root, options) => {
62
66
  )}\n`
63
67
  );
64
68
 
65
- // send to S3 using the Asset Store API
66
- const assetStore = new AssetStoreUploader({
67
- apiUrl,
68
- apiKey,
69
- basePath: remoteBasePath,
70
- });
69
+ try {
70
+ // send to S3 using the Asset Store API
71
+ const assetStore = new AssetStoreUploader({
72
+ apiUrl,
73
+ apiKey,
74
+ basePath: remoteBasePath,
75
+ });
71
76
 
72
- const uploadResult = await assetStore.uploadDir(localDir);
77
+ const uploadResult = await assetStore.uploadDir(localDir);
73
78
 
74
- log.success(
75
- `Publish complete in ${(uploadResult.duration / 1000).toFixed(0)}s\n`
76
- );
79
+ // validate & upload local configs to configuration API
80
+ await validateLocalConfigs(fertConfig);
81
+ await updateProjectConfigs(fertConfig),
82
+ {
83
+ environment: fertConfig.currBranch === 'master' ? 'production' : 'dev',
84
+ };
77
85
 
78
- // invalidate cloudfront cache
79
- await assetStore.invalidatePaths([assetStoreInvPath]);
86
+ log.success(
87
+ `Publish complete in ${(uploadResult.duration / 1000).toFixed(0)}s\n`
88
+ );
80
89
 
81
- for (let dist of clientCloudFrontDists) {
82
- await assetStore.invalidatePaths([brandedSiteInvPath], dist.id);
83
- }
90
+ // invalidate cloudfront cache
91
+ await assetStore.invalidatePaths([assetStoreInvPath]);
84
92
 
85
- log.info(
86
- `\nNote cloudfront invalidations may take upto ${chalk.yellow(
87
- '30 seconds'
88
- )} to complete.\n`
89
- );
93
+ for (let dist of clientCloudFrontDists) {
94
+ await assetStore.invalidatePaths([brandedSiteInvPath], dist.id);
95
+ }
96
+
97
+ log.info(
98
+ `\nNote cloudfront invalidations may take upto ${chalk.yellow(
99
+ '30 seconds'
100
+ )} to complete.\n`
101
+ );
102
+ } catch (error) {
103
+ log.error('An error occurred during the publish process', error);
104
+ }
90
105
  };
@@ -0,0 +1,208 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const Hoek = require('@hapi/hoek');
4
+ const { log } = require('../utils/logging');
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+
8
+ /**
9
+ * Validates the local configuration files against the provided schema.
10
+ *
11
+ * @param {Object} fertConfig - The project config.
12
+ * @param {Object} options - The options object.
13
+ * @param {boolean} options.catch - Whether to catch validation errors or just log them.
14
+ *
15
+ * @returns {Promise<void>} - Returns a promise that resolves if all configurations are valid.
16
+ * @throws {Error} - Throws an error if any configuration is invalid.
17
+ */
18
+ const validateLocalConfigs = async (fertConfig, options = { catch: true }) => {
19
+ Hoek.assert(fertConfig, 'fertConfig is required');
20
+
21
+ const api = await getConfigAPI({
22
+ clientPropertyId: fertConfig.clientPropertyId,
23
+ });
24
+ const localConfigs = loadLocalConfigs(fertConfig);
25
+
26
+ const validationPromises = Object.keys(localConfigs).map(
27
+ async (configName) => {
28
+ try {
29
+ const schema = api.getSchema(configName);
30
+ const { error } = schema.validate(localConfigs[configName].data);
31
+ if (error) {
32
+ throw new Error(
33
+ `Config invalid: ${chalk.bold(localConfigs[configName].path)} - ${error.message}`
34
+ );
35
+ }
36
+
37
+ log.success(`Config validated:`, chalk.bold(configName));
38
+ } catch (err) {
39
+ log.error(err.message);
40
+
41
+ if (!options.catch) {
42
+ throw err;
43
+ }
44
+ }
45
+ }
46
+ );
47
+
48
+ await Promise.all(validationPromises);
49
+ };
50
+
51
+ const getConfigAPI = async ({
52
+ clientPropertyId,
53
+ environment = 'production',
54
+ } = {}) => {
55
+ const { ConfigAPI } = await import('@madgex/config-api-sdk');
56
+
57
+ const api = new ConfigAPI({
58
+ clientPropertyId,
59
+ environment,
60
+ });
61
+
62
+ await api.loadSchemas();
63
+
64
+ return api;
65
+ };
66
+
67
+ const loadLocalConfigs = (fertConfig) => {
68
+ const dir = path.join(fertConfig.workingDir, 'config');
69
+ const configs = {};
70
+
71
+ if (!fs.existsSync(dir)) {
72
+ throw new Error(`Directory does not exist: ${dir}`);
73
+ }
74
+
75
+ const files = fs.readdirSync(dir);
76
+
77
+ files.forEach((file) => {
78
+ const filePath = path.join(dir, file);
79
+
80
+ if (fs.statSync(filePath).isFile()) {
81
+ const fileName = path.basename(file, path.extname(file));
82
+
83
+ // Purge any existing cache of the file
84
+ delete require.cache[filePath];
85
+
86
+ try {
87
+ configs[fileName] = {
88
+ path: filePath,
89
+ data: require(filePath),
90
+ };
91
+ } catch (err) {
92
+ log.error(`Error loading config: ${err.message}`);
93
+ }
94
+ }
95
+ });
96
+
97
+ return configs;
98
+ };
99
+
100
+ /**
101
+ * Handles the removal and setting of project configs in the Configuration API.
102
+ *
103
+ * @param {Object} fertConfig - The project config.
104
+ *
105
+ * @returns {Promise<boolean>} - Returns a promise that resolves to true if the update is successful.
106
+ */
107
+ const updateProjectConfigs = async (
108
+ fertConfig,
109
+ { environment = 'production' } = {}
110
+ ) => {
111
+ Hoek.assert(fertConfig, 'fertConfig is required');
112
+
113
+ let spinner;
114
+
115
+ try {
116
+ const api = await getConfigAPI({
117
+ clientPropertyId: fertConfig.clientPropertyId,
118
+ environment,
119
+ });
120
+ spinner = ora('Fetching local configs').start();
121
+
122
+ const localConfigs = loadLocalConfigs(fertConfig);
123
+ const { toRemove, toSet } = collateConfigs(api, localConfigs);
124
+
125
+ spinner.text = 'Updating configs…';
126
+ await Promise.all([removeConfigs(api, toRemove), setConfigs(api, toSet)]);
127
+
128
+ const { host } = new URL(api.options.apiUrl);
129
+ spinner.succeed(`Project configs applied to ${chalk.bold(host)}`);
130
+
131
+ return true;
132
+ } catch (error) {
133
+ spinner.fail('Failed to apply project configs');
134
+ log.error(error);
135
+ throw error;
136
+ }
137
+ };
138
+
139
+ const collateConfigs = (api, localConfigs) => {
140
+ const toRemove = {};
141
+ const toSet = {};
142
+
143
+ Object.entries(localConfigs).forEach(
144
+ ([configName, { data: config = {} } = {}]) => {
145
+ const configDefault = api._getDefaultConfig(configName);
146
+
147
+ const unsetKeysWithDefaults = getUnsetKeysWithDefaults(
148
+ configDefault,
149
+ config
150
+ );
151
+ toRemove[configName] = Object.keys(unsetKeysWithDefaults);
152
+ toSet[configName] = config;
153
+ }
154
+ );
155
+
156
+ return { toRemove, toSet };
157
+ };
158
+
159
+ const getUnsetKeysWithDefaults = (defaults = {}, config = {}) => {
160
+ Hoek.assert(
161
+ typeof defaults === 'object' && defaults !== null,
162
+ 'defaults must be an object'
163
+ );
164
+ Hoek.assert(
165
+ typeof config === 'object' && config !== null,
166
+ 'config must be an object'
167
+ );
168
+
169
+ const diff = {};
170
+ for (const key in defaults) {
171
+ if (!Object.keys(config).includes(key)) {
172
+ diff[key] = defaults[key];
173
+ }
174
+ }
175
+ return diff;
176
+ };
177
+
178
+ const removeConfigs = async (api, toRemove) => {
179
+ const deletePromises = Object.entries(toRemove).flatMap(
180
+ ([configName, keys]) =>
181
+ keys.map((key) =>
182
+ api.deleteConfig(configName, key).catch((error) => {
183
+ log.error('Failed to remove config', { configName, key, error });
184
+ })
185
+ )
186
+ );
187
+
188
+ await Promise.all(deletePromises);
189
+ };
190
+
191
+ const setConfigs = async (api, toSet) => {
192
+ const setPromises = Object.entries(toSet).flatMap(([configName, config]) =>
193
+ Object.entries(config).map(async ([key, value]) => {
194
+ try {
195
+ await api.setConfig(configName, key, value);
196
+ } catch (error) {
197
+ log.error('Failed to set config', { configName, key, value, error });
198
+ }
199
+ })
200
+ );
201
+
202
+ await Promise.all(setPromises);
203
+ };
204
+ module.exports = {
205
+ getConfigAPI,
206
+ validateLocalConfigs,
207
+ updateProjectConfigs,
208
+ };
@@ -4,19 +4,19 @@ const { VERSION, SITE } = require('../../constants');
4
4
  const chalk = require('chalk');
5
5
  const uuidValidator = require('uuid-validate');
6
6
  const Hoek = require('@hapi/hoek');
7
+ const simpleGit = require('simple-git');
7
8
  const { cpidLookup } = require('./cpid-lookup');
8
9
  const { cpIdMatchesGitRemote } = require('./cpid-matches-git-remote');
9
10
  const { resolveExternalAssets } = require('./resolve-external-assets');
10
11
  const { log } = require('./logging');
11
12
  const { CONFIG_API } = require('../../constants');
12
- const assert = require('node:assert');
13
13
 
14
14
  exports.printBanner = () => {
15
15
  console.log(`\n${chalk.green.bold('Fert')} v${VERSION}`);
16
16
  };
17
17
 
18
18
  exports.getConfig = async (cpid, configNames = []) => {
19
- assert(Array.isArray(configNames), 'configNames must be an array');
19
+ Hoek.assert(Array.isArray(configNames), 'configNames must be an array');
20
20
 
21
21
  const result = {};
22
22
 
@@ -42,6 +42,7 @@ exports.resolveConfig = async (root, options = {}) => {
42
42
  };
43
43
 
44
44
  const workingDir = root ? path.resolve(root) : path.resolve(process.cwd());
45
+ const git = simpleGit({ baseDir: workingDir });
45
46
 
46
47
  const fertConfig = Hoek.applyToDefaults(
47
48
  defaults,
@@ -54,8 +55,10 @@ exports.resolveConfig = async (root, options = {}) => {
54
55
  `Invalid clientPropertyId specified in fert.config.js: ${fertConfig.clientPropertyId}`
55
56
  );
56
57
 
57
- // Check Fert cpid matches git's remote
58
+ // get & store the current git branch
59
+ fertConfig.currBranch = (await git.branch()).current;
58
60
 
61
+ // Check Fert cpid matches git's remote
59
62
  const fertCpidMatchesGitRemote = await cpIdMatchesGitRemote(
60
63
  fertConfig,
61
64
  workingDir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madgex/fert",
3
- "version": "3.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "Tool to help build the V6 branding",
5
5
  "bin": {
6
6
  "fert": "./bin/cli.js"
@@ -18,7 +18,7 @@
18
18
  "url": "https://github.com/wiley/madgex-frontend-rollout-tool.git"
19
19
  },
20
20
  "engines": {
21
- "node": ">=16.0.0"
21
+ "node": ">=18.20.0"
22
22
  },
23
23
  "author": "Madgex",
24
24
  "license": "UNLICENSED",
@@ -31,7 +31,8 @@
31
31
  "@hapi/inert": "^7.1.0",
32
32
  "@hapi/vision": "^7.0.3",
33
33
  "@hapipal/toys": "^4.0.0",
34
- "@madgex/design-system": "^6.1.4",
34
+ "@madgex/config-api-sdk": "^1.0.6",
35
+ "@madgex/design-system": "^7.0.0",
35
36
  "@private/header-footer-podlet-server": "github:wiley/madgex-header-footer-podlet",
36
37
  "axios": "^1.6.2",
37
38
  "cac": "^6.7.14",
@@ -28,7 +28,7 @@
28
28
  </div>
29
29
  <div class="mds-grid-col-12 mds-grid-col-lg-3">
30
30
  <div class="mds-branded-container mds-branded-container--1 mds-padding-b4 mds-padding-bottom-b5 mds-margin-bottom-b5">
31
- <h2 class="mds-font-great-primer">Branded Container</h2>
31
+ <h2 class="mds-font-s2">Branded Container</h2>
32
32
  <p class="mds-margin-bottom-b4">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
33
33
  <a href="#" draggable="false" class="mds-button mds-button--neutral mds-button--small">Lorem ipsum</a>
34
34
  </div>