@madgex/fert 7.0.13 → 7.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 +3 -0
- package/bin/commands/build.js +13 -0
- package/bin/commands/dev-server.js +5 -0
- package/bin/commands/publish-tasks/asset-store-uploader.js +40 -35
- package/bin/commands/publish.js +11 -12
- package/bin/commands/validate.js +27 -0
- package/bin/utils/validation-helpers.js +87 -0
- package/bin/validators/brand-json-validator.js +160 -0
- package/package.json +17 -19
- package/shared/Dockerfile.brandingRepoPublish +1 -1
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { printBanner } from './utils/index.js';
|
|
|
10
10
|
import { serviceCommandBootstrap } from './commands/_service-command-bootstrap.js';
|
|
11
11
|
import { configsCommand } from './commands/configs.js';
|
|
12
12
|
import { initCommand } from './commands/init.js';
|
|
13
|
+
import { validateCommand } from './commands/validate.js';
|
|
13
14
|
|
|
14
15
|
const cli = cac('fert');
|
|
15
16
|
|
|
@@ -56,6 +57,8 @@ const run = () => {
|
|
|
56
57
|
.option('--cpid <cpid>', 'Specify the clientPropertyId to use in the new project')
|
|
57
58
|
.action((...args) => initCommand(...args));
|
|
58
59
|
|
|
60
|
+
cli.command('validate', 'Validate branding project for common issues').action((...args) => validateCommand(...args));
|
|
61
|
+
|
|
59
62
|
cli.option('--no-cache', 'Do not use cache');
|
|
60
63
|
cli.option('--purge-cache', 'Purge all caches');
|
|
61
64
|
cli.help();
|
package/bin/commands/build.js
CHANGED
|
@@ -6,6 +6,10 @@ import { bundleEntry } from './build-tasks/bundle-entry.js';
|
|
|
6
6
|
import { buildCssFromTokens } from './build-tasks/build-tokens.js';
|
|
7
7
|
import { buildExternalAssets } from './build-tasks/build-external-assets.js';
|
|
8
8
|
import { validateLocalConfigs } from '../utils/configs.js';
|
|
9
|
+
import { runValidators, reportResults } from '../utils/validation-helpers.js';
|
|
10
|
+
import * as brandJsonValidator from '../validators/brand-json-validator.js';
|
|
11
|
+
|
|
12
|
+
const validators = [brandJsonValidator];
|
|
9
13
|
|
|
10
14
|
export { bundleEntry, buildExternalAssets };
|
|
11
15
|
|
|
@@ -22,6 +26,15 @@ export async function build(options = {}) {
|
|
|
22
26
|
clientPropertyId: fertConfig.clientPropertyId,
|
|
23
27
|
});
|
|
24
28
|
|
|
29
|
+
const validationResults = await runValidators(validators, {
|
|
30
|
+
rootDir: fertConfig.rootDir,
|
|
31
|
+
options,
|
|
32
|
+
});
|
|
33
|
+
const validationExitCode = reportResults(validationResults, fertConfig.rootDir);
|
|
34
|
+
if (validationExitCode !== 0) {
|
|
35
|
+
throw new Error('Build aborted due to validation errors');
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
if (!options.only) {
|
|
26
39
|
await rimraf(path.resolve(fertConfig.workingDir, 'dist'));
|
|
27
40
|
await buildTokens(fertConfig, options);
|
|
@@ -6,9 +6,11 @@ import open from 'open';
|
|
|
6
6
|
import chokidar from 'chokidar5';
|
|
7
7
|
import { resolveConfig, findFertConfigDir } from '../utils/index.js';
|
|
8
8
|
import { validateLocalConfigs } from '../utils/configs.js';
|
|
9
|
+
import { runValidators, reportResults } from '../utils/validation-helpers.js';
|
|
9
10
|
import { devServer } from '../../server/server.js';
|
|
10
11
|
import { buildTokens, buildExternalAssets } from './build.js';
|
|
11
12
|
import { BRAND_JSON_FILENAME, FERT_CONFIG_FILENAME, FERT_SERVICE_CONFIG_FILENAME } from '../../constants.js';
|
|
13
|
+
import * as brandJsonValidator from '../validators/brand-json-validator.js';
|
|
12
14
|
|
|
13
15
|
export async function createDevServer(options = {}) {
|
|
14
16
|
let fertConfig = await resolveConfig(options);
|
|
@@ -43,6 +45,9 @@ export async function createDevServer(options = {}) {
|
|
|
43
45
|
|
|
44
46
|
chokidar.watch(brandPath, { ignoreInitial: true }).on('all', async () => {
|
|
45
47
|
await buildTokens(fertConfig);
|
|
48
|
+
|
|
49
|
+
const results = await runValidators([brandJsonValidator], { rootDir: fertConfig.rootDir, options });
|
|
50
|
+
reportResults(results, rootDir);
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
const configPath = path.resolve(fertConfigDir, FERT_CONFIG_FILENAME);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import * as Hoek from '@hapi/hoek';
|
|
4
|
-
import axios from 'axios';
|
|
5
|
-
import FormData from 'form-data';
|
|
6
4
|
import ora from 'ora';
|
|
7
5
|
import chalk from 'chalk';
|
|
8
6
|
import { log } from '../../utils/logging.js';
|
|
@@ -27,43 +25,52 @@ export class AssetStoreUploader {
|
|
|
27
25
|
const label = absoluteUrl.pathname;
|
|
28
26
|
|
|
29
27
|
log.debug('upload', { file, dest, absoluteUrl });
|
|
30
|
-
|
|
28
|
+
// `openAsBlob` returns a "File-backend" Blob, not in memory
|
|
29
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
30
|
+
const fileBackedBlob = await fs.openAsBlob(file);
|
|
31
31
|
const form = new FormData();
|
|
32
|
-
form.append('data',
|
|
32
|
+
form.append('data', fileBackedBlob, path.basename(file));
|
|
33
33
|
form.append('invalidateCache', 'false'); // opt out as will invalidate whole path (*) at end
|
|
34
34
|
form.append('modifiedById', ASSET_STORE_USER_GUID);
|
|
35
35
|
|
|
36
36
|
const spinner = ora(`${chalk.yellow('Uploading')}: ${label}`).start();
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(absoluteUrl.href, {
|
|
40
|
+
method: 'put',
|
|
40
41
|
headers: {
|
|
41
42
|
apikey: this.apiKey,
|
|
42
|
-
...form.getHeaders(),
|
|
43
43
|
},
|
|
44
|
-
|
|
45
|
-
.then(() => {
|
|
46
|
-
spinner.succeed(`${label}`);
|
|
47
|
-
})
|
|
48
|
-
.catch((err) => {
|
|
49
|
-
spinner.fail(`Failed to upload '${label}':`, err.message);
|
|
50
|
-
|
|
51
|
-
throw err;
|
|
44
|
+
body: form,
|
|
52
45
|
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const resText = await res.text();
|
|
48
|
+
throw new Error(`${res.status} - ${resText}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
spinner.succeed(`${label}`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
spinner.fail(`Failed to upload '${label}':`, err.message);
|
|
54
|
+
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
async uploadDir(dir,
|
|
59
|
+
async uploadDir(dir, dryRun = false) {
|
|
56
60
|
Hoek.assert(dir, 'dir missing');
|
|
57
61
|
|
|
58
62
|
const start = process.hrtime();
|
|
59
|
-
const files = this._getFilesFromDir(dir);
|
|
63
|
+
const files = await this._getFilesFromDir(dir);
|
|
60
64
|
|
|
61
65
|
// Send files consecutively to avoid straining the api
|
|
62
66
|
for (const file of files) {
|
|
63
67
|
const relativeName = path.posix.relative(dir, file);
|
|
64
|
-
const remotePath = path.posix.dirname(
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const remotePath = path.posix.dirname(relativeName);
|
|
69
|
+
if (!dryRun) {
|
|
70
|
+
await this.upload(file, remotePath);
|
|
71
|
+
} else {
|
|
72
|
+
log.info('(Dry run) Uploading', file, 'to => ', remotePath);
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
const end = process.hrtime(start);
|
|
@@ -94,10 +101,13 @@ export class AssetStoreUploader {
|
|
|
94
101
|
payload.distributionId = distributionId;
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
await
|
|
104
|
+
await fetch(invalidateUrl.href, {
|
|
105
|
+
method: 'post',
|
|
98
106
|
headers: {
|
|
99
107
|
apikey: this.apiKey,
|
|
108
|
+
'Content-Type': 'application/json',
|
|
100
109
|
},
|
|
110
|
+
body: JSON.stringify(payload),
|
|
101
111
|
});
|
|
102
112
|
|
|
103
113
|
log.info(
|
|
@@ -106,18 +116,13 @@ export class AssetStoreUploader {
|
|
|
106
116
|
}:\n${JSON.stringify(paths)}\n`,
|
|
107
117
|
);
|
|
108
118
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return files;
|
|
122
|
-
};
|
|
119
|
+
/**
|
|
120
|
+
* Returns array of all files in provided directory (recursive), as absolute paths including filename.
|
|
121
|
+
*/
|
|
122
|
+
async _getFilesFromDir(dir) {
|
|
123
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
124
|
+
const all = await Array.fromAsync(fs.promises.glob('**/*', { cwd: dir, withFileTypes: true })); // withFileTypes return dirent instead of strings
|
|
125
|
+
// only files, make sure output is array of absolute file path strings
|
|
126
|
+
return all.filter((dirent) => dirent.isFile()).map((dirent) => path.resolve(dirent.parentPath, dirent.name));
|
|
127
|
+
}
|
|
123
128
|
}
|
package/bin/commands/publish.js
CHANGED
|
@@ -57,10 +57,10 @@ export async function publish(options) {
|
|
|
57
57
|
const localDir = path.resolve(fertConfig.workingDir, UPLOAD_DIR);
|
|
58
58
|
const apiKeyParamPath = Hoek.reachTemplate(templateCtx, AWS_PARAM_NAME);
|
|
59
59
|
|
|
60
|
-
const apiKey = await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const [apiKey, assetsPath] = await Promise.all([
|
|
61
|
+
getAwsParam(apiKeyParamPath),
|
|
62
|
+
getAssetsPath(fertConfig, options.target),
|
|
63
|
+
]);
|
|
64
64
|
|
|
65
65
|
log.info(
|
|
66
66
|
`\nPublishing ${chalk.cyan(localDir)} to ${chalk.green.bold(
|
|
@@ -76,18 +76,17 @@ export async function publish(options) {
|
|
|
76
76
|
basePath: remoteBasePath,
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
if (!options.dryRun) {
|
|
81
|
-
uploadResult = await assetStore.uploadDir(localDir);
|
|
82
|
-
}
|
|
79
|
+
const uploadResult = await assetStore.uploadDir(localDir, options.dryRun);
|
|
83
80
|
|
|
84
81
|
log.success(`Publish complete in ${((uploadResult?.duration || 1) / 1000).toFixed(0)}s\n`);
|
|
85
82
|
|
|
86
83
|
// invalidate cloudfront cache
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
if (!options.dryRun) {
|
|
85
|
+
await assetStore.invalidatePaths([assetStoreInvPath]);
|
|
86
|
+
const clientCloudFrontDists = await getCloudFrontDistributionsForDomain(assetsPath.hostname, options);
|
|
87
|
+
for (let dist of clientCloudFrontDists) {
|
|
88
|
+
await assetStore.invalidatePaths([`${assetsPath.pathname}/*`], dist.id);
|
|
89
|
+
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
log.info(`\nNote cloudfront invalidations may take upto ${chalk.yellow('30 seconds')} to complete.\n`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { findFertConfigDir } from '../utils/index.js';
|
|
2
|
+
import { runValidators, reportResults } from '../utils/validation-helpers.js';
|
|
3
|
+
import { log } from '../utils/logging.js';
|
|
4
|
+
|
|
5
|
+
// Register all validators here
|
|
6
|
+
import * as brandJsonValidator from '../validators/brand-json-validator.js';
|
|
7
|
+
|
|
8
|
+
const validators = [brandJsonValidator];
|
|
9
|
+
|
|
10
|
+
export async function validateCommand(options) {
|
|
11
|
+
log.info('Running validations…\n');
|
|
12
|
+
|
|
13
|
+
const rootDir = await findFertConfigDir();
|
|
14
|
+
|
|
15
|
+
const context = {
|
|
16
|
+
rootDir,
|
|
17
|
+
options,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const results = await runValidators(validators, context);
|
|
21
|
+
const exitCode = reportResults(results, rootDir);
|
|
22
|
+
|
|
23
|
+
if (exitCode !== 0) {
|
|
24
|
+
// eslint-disable-next-line n/no-process-exit
|
|
25
|
+
process.exit(exitCode);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from './logging.js';
|
|
4
|
+
|
|
5
|
+
export const Severity = Object.freeze({
|
|
6
|
+
ERROR: 'ERROR',
|
|
7
|
+
WARNING: 'WARNING',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a validation result entry.
|
|
12
|
+
*/
|
|
13
|
+
export function createResult(severity, message, { filePath, detail } = {}) {
|
|
14
|
+
return { severity, message, filePath, detail };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs an array of validator functions and collects all results.
|
|
19
|
+
* Each validator receives `context` and must return an array of result objects.
|
|
20
|
+
*/
|
|
21
|
+
export async function runValidators(validators, context) {
|
|
22
|
+
const results = [];
|
|
23
|
+
|
|
24
|
+
for (const validator of validators) {
|
|
25
|
+
const validatorResults = await validator.validate(context);
|
|
26
|
+
results.push(...validatorResults);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prints validation results — errors first, then warnings grouped by file.
|
|
34
|
+
* Paths are shown relative to rootDir.
|
|
35
|
+
* Returns 1 if any ERROR results exist, 0 otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export function reportResults(results, rootDir = process.cwd()) {
|
|
38
|
+
const errors = results.filter((r) => r.severity === Severity.ERROR);
|
|
39
|
+
const warnings = results.filter((r) => r.severity === Severity.WARNING);
|
|
40
|
+
|
|
41
|
+
if (errors.length) {
|
|
42
|
+
console.log(chalk.red.bold(`\n✖ ${errors.length} error${errors.length === 1 ? '' : 's'}\n`));
|
|
43
|
+
printGrouped(groupByFile(errors), rootDir, Severity.ERROR);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (warnings.length) {
|
|
47
|
+
console.log(chalk.yellow.bold(`\n⚠ ${warnings.length} warning${warnings.length === 1 ? '' : 's'}\n`));
|
|
48
|
+
printGrouped(groupByFile(warnings), rootDir, Severity.WARNING);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!errors.length && !warnings.length) {
|
|
52
|
+
log.success('All validations passed');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
return errors.length > 0 ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function groupByFile(results) {
|
|
61
|
+
const groups = new Map();
|
|
62
|
+
for (const result of results) {
|
|
63
|
+
const key = result.filePath ?? null;
|
|
64
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
65
|
+
groups.get(key).push(result);
|
|
66
|
+
}
|
|
67
|
+
return groups;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printGrouped(groups, rootDir, severity) {
|
|
71
|
+
const color = severity === Severity.ERROR ? chalk.red : chalk.yellow;
|
|
72
|
+
const symbol = severity === Severity.ERROR ? '✖' : '⚠';
|
|
73
|
+
|
|
74
|
+
for (const [filePath, items] of groups) {
|
|
75
|
+
if (filePath) {
|
|
76
|
+
console.log(chalk.dim(` ${path.relative(rootDir, filePath)}`));
|
|
77
|
+
}
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const indent = filePath ? ' ' : ' ';
|
|
80
|
+
console.log(color.bold(symbol), `${indent}${item.message}`);
|
|
81
|
+
if (item.detail) {
|
|
82
|
+
console.log(chalk.dim(` ${indent}${item.detail}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createStyleDictionary } from '@madgex/design-system/style-dictionary';
|
|
4
|
+
// eslint-disable-next-line n/no-extraneous-import
|
|
5
|
+
import { resolveReferences } from 'style-dictionary/utils';
|
|
6
|
+
import { loadServiceConfigFiles } from '../utils/index.js';
|
|
7
|
+
import { Severity, createResult } from '../utils/validation-helpers.js';
|
|
8
|
+
import { BRAND_JSON_FILENAME } from '../../constants.js';
|
|
9
|
+
|
|
10
|
+
export const name = 'brand-json';
|
|
11
|
+
export const description = 'Validates brand.json overrides against the MDS base token schema';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Recursively extracts all dot-separated token paths from a token object.
|
|
15
|
+
* Stops at `$value` leaves and skips `$`-prefixed metadata keys.
|
|
16
|
+
*/
|
|
17
|
+
export function extractTokenPaths(obj, prefix = '') {
|
|
18
|
+
const paths = new Set();
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (key.startsWith('$')) continue;
|
|
22
|
+
|
|
23
|
+
const currentPath = prefix ? `${prefix}.${key}` : key;
|
|
24
|
+
|
|
25
|
+
if (value && typeof value === 'object' && !('$value' in value)) {
|
|
26
|
+
const childPaths = extractTokenPaths(value, currentPath);
|
|
27
|
+
if (childPaths.size > 0) {
|
|
28
|
+
for (const childPath of childPaths) {
|
|
29
|
+
paths.add(childPath);
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// Empty group — emit the path itself so it can be validated against the schema
|
|
33
|
+
paths.add(currentPath);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
paths.add(currentPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return paths;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recursively resolves a token `$value` through any `{references}` to a final scalar.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveToFinalValue(rawValue, tokens) {
|
|
47
|
+
if (typeof rawValue !== 'string' || !rawValue.includes('{')) return rawValue;
|
|
48
|
+
const resolved = resolveReferences(rawValue, tokens, { usesDtcg: true});
|
|
49
|
+
if (resolved) {
|
|
50
|
+
return resolveToFinalValue(resolved, tokens);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Walks a nested object by dot-separated path and returns the `$value` at that leaf.
|
|
57
|
+
*/
|
|
58
|
+
export function getValueAtPath(obj, dotPath) {
|
|
59
|
+
let current = obj;
|
|
60
|
+
for (const part of dotPath.split('.')) {
|
|
61
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
return current?.$value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Loads all MDS base tokens via StyleDictionary and returns the set of valid
|
|
69
|
+
* token paths, their resolved values, and the tokens tree for reference resolution.
|
|
70
|
+
*/
|
|
71
|
+
async function loadBaseSchema() {
|
|
72
|
+
const { styleDictionary, cleanTempFiles } = await createStyleDictionary();
|
|
73
|
+
const basePaths = new Set();
|
|
74
|
+
const baseResolvedValues = new Map();
|
|
75
|
+
|
|
76
|
+
for (const token of styleDictionary.allTokens) {
|
|
77
|
+
const tokenPath = token.key.slice(1, -1);
|
|
78
|
+
basePaths.add(tokenPath);
|
|
79
|
+
baseResolvedValues.set(tokenPath, resolveToFinalValue(token.$value, styleDictionary.tokens));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseTokens = styleDictionary.tokens;
|
|
83
|
+
await cleanTempFiles();
|
|
84
|
+
return { basePaths, baseResolvedValues, baseTokens };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function validate() {
|
|
88
|
+
const results = [];
|
|
89
|
+
const { basePaths, baseResolvedValues, baseTokens } = await loadBaseSchema();
|
|
90
|
+
|
|
91
|
+
let serviceConfigs;
|
|
92
|
+
try {
|
|
93
|
+
serviceConfigs = await loadServiceConfigFiles();
|
|
94
|
+
} catch {
|
|
95
|
+
results.push(
|
|
96
|
+
createResult(Severity.WARNING, 'Could not discover services — skipping brand.json validation', {
|
|
97
|
+
detail: 'Ensure you are running from a branding repo with a fert.config.js',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const { dir } of serviceConfigs) {
|
|
104
|
+
const brandJsonPath = path.join(dir, BRAND_JSON_FILENAME);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(brandJsonPath)) {
|
|
107
|
+
results.push(
|
|
108
|
+
createResult(Severity.WARNING, `Missing ${BRAND_JSON_FILENAME}`, {
|
|
109
|
+
filePath: brandJsonPath,
|
|
110
|
+
detail: 'Each service should have a brand.json for token overrides',
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let brandData;
|
|
117
|
+
try {
|
|
118
|
+
brandData = JSON.parse(fs.readFileSync(brandJsonPath, 'utf-8'));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
results.push(
|
|
121
|
+
createResult(Severity.ERROR, `Invalid JSON in ${BRAND_JSON_FILENAME}`, {
|
|
122
|
+
filePath: brandJsonPath,
|
|
123
|
+
detail: err.message,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const overridePaths = extractTokenPaths(brandData);
|
|
130
|
+
|
|
131
|
+
for (const overridePath of overridePaths) {
|
|
132
|
+
if (!basePaths.has(overridePath)) {
|
|
133
|
+
results.push(
|
|
134
|
+
createResult(Severity.WARNING, `Unknown token path: "${overridePath}"`, {
|
|
135
|
+
filePath: brandJsonPath,
|
|
136
|
+
detail: 'This path does not exist in the MDS base schema and will have no effect',
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rawOverrideValue = getValueAtPath(brandData, overridePath);
|
|
143
|
+
if (rawOverrideValue === undefined) continue;
|
|
144
|
+
|
|
145
|
+
const resolvedOverride = resolveToFinalValue(rawOverrideValue, baseTokens);
|
|
146
|
+
const resolvedBase = baseResolvedValues.get(overridePath);
|
|
147
|
+
|
|
148
|
+
if (resolvedOverride === resolvedBase) {
|
|
149
|
+
results.push(
|
|
150
|
+
createResult(Severity.WARNING, `Redundant override: "${overridePath}"`, {
|
|
151
|
+
filePath: brandJsonPath,
|
|
152
|
+
detail: `Value "${String(rawOverrideValue)}" resolves to the same value as the MDS base ("${String(resolvedBase)}")`,
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madgex/fert",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Tool to help build the V6 branding",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fert": "./bin/cli.js"
|
|
@@ -24,47 +24,45 @@
|
|
|
24
24
|
"author": "Madgex",
|
|
25
25
|
"license": "UNLICENSED",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@aws-sdk/client-cloudfront": "3.
|
|
28
|
-
"@aws-sdk/client-ssm": "3.
|
|
29
|
-
"@hapi/hapi": "21.4.
|
|
27
|
+
"@aws-sdk/client-cloudfront": "3.1017.0",
|
|
28
|
+
"@aws-sdk/client-ssm": "3.1017.0",
|
|
29
|
+
"@hapi/hapi": "21.4.7",
|
|
30
30
|
"@hapi/hoek": "11.0.7",
|
|
31
31
|
"@hapi/inert": "7.1.0",
|
|
32
32
|
"@hapi/vision": "7.0.3",
|
|
33
33
|
"@hapipal/toys": "4.0.0",
|
|
34
|
-
"@madgex/config-api-sdk": "1.
|
|
35
|
-
"@madgex/design-system": "13.
|
|
34
|
+
"@madgex/config-api-sdk": "1.16.0",
|
|
35
|
+
"@madgex/design-system": "13.5.1",
|
|
36
36
|
"@private/header-footer-podlet-server": "github:wiley/madgex-header-footer-podlet",
|
|
37
|
-
"axios": "1.13.2",
|
|
38
37
|
"cac": "6.7.14",
|
|
39
38
|
"chalk": "5.6.2",
|
|
40
39
|
"chokidar5": "npm:chokidar@5.0.0",
|
|
41
40
|
"debug": "4.4.3",
|
|
42
41
|
"dedent": "1.7.1",
|
|
43
42
|
"find-up-simple": "1.0.1",
|
|
44
|
-
"flat-cache": "6.1.
|
|
45
|
-
"
|
|
46
|
-
"joi": "18.0.2",
|
|
43
|
+
"flat-cache": "6.1.21",
|
|
44
|
+
"joi": "18.1.1",
|
|
47
45
|
"nunjucks": "3.2.4",
|
|
48
46
|
"open": "11.0.0",
|
|
49
47
|
"ora": "5.4.1",
|
|
50
48
|
"prompts": "2.4.2",
|
|
51
49
|
"rimraf": "6.1.2",
|
|
52
|
-
"sass": "1.
|
|
53
|
-
"simple-git": "3.
|
|
50
|
+
"sass": "1.98.0",
|
|
51
|
+
"simple-git": "3.33.0",
|
|
54
52
|
"simple-update-notifier": "2.0.0",
|
|
55
53
|
"uuid-validate": "0.0.3",
|
|
56
54
|
"vite": "7.3.1",
|
|
57
55
|
"vite-plugin-static-copy": "3.2.0"
|
|
58
56
|
},
|
|
59
57
|
"devDependencies": {
|
|
60
|
-
"@commitlint/cli": "20.
|
|
61
|
-
"@commitlint/config-conventional": "20.
|
|
62
|
-
"@madgex/eslint-config-madgex": "
|
|
63
|
-
"@madgex/prettier-config-madgex": "2.0.
|
|
64
|
-
"eslint": "
|
|
58
|
+
"@commitlint/cli": "20.5.0",
|
|
59
|
+
"@commitlint/config-conventional": "20.5.0",
|
|
60
|
+
"@madgex/eslint-config-madgex": "3.0.0",
|
|
61
|
+
"@madgex/prettier-config-madgex": "2.0.1",
|
|
62
|
+
"eslint": "10.1.0",
|
|
65
63
|
"husky": "9.1.7",
|
|
66
|
-
"lint-staged": "16.
|
|
67
|
-
"msw": "^2.12.
|
|
64
|
+
"lint-staged": "16.4.0",
|
|
65
|
+
"msw": "^2.12.14",
|
|
68
66
|
"prettier": "3.8.1",
|
|
69
67
|
"semantic-release": "25.0.3"
|
|
70
68
|
},
|