@madgex/fert 7.1.1 → 7.2.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 +13 -3
- package/bin/commands/_root-tasks-bootstrap.js +45 -0
- package/bin/commands/_service-command-bootstrap.js +10 -3
- package/bin/commands/build.js +7 -18
- package/bin/commands/dev-server.js +29 -33
- package/bin/commands/publish.js +5 -0
- package/bin/commands/validate.js +16 -20
- package/bin/utils/cpid-lookup.js +2 -2
- package/bin/utils/index.js +21 -6
- package/bin/utils/validation.js +145 -0
- package/bin/validators/{brand-json-validator.js → brand-json.validator.js} +49 -63
- package/bin/validators/translations.validator.js +137 -0
- package/constants.js +10 -8
- package/jsconfig.json +10 -0
- package/package.json +4 -4
- package/types.d.ts +54 -0
- package/bin/utils/validation-helpers.js +0 -87
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { serviceCommandBootstrap } from './commands/_service-command-bootstrap.j
|
|
|
11
11
|
import { configsCommand } from './commands/configs.js';
|
|
12
12
|
import { initCommand } from './commands/init.js';
|
|
13
13
|
import { validateCommand } from './commands/validate.js';
|
|
14
|
+
import { rootTasksBootstrap } from './commands/_root-tasks-bootstrap.js';
|
|
14
15
|
|
|
15
16
|
const cli = cac('fert');
|
|
16
17
|
|
|
@@ -24,7 +25,10 @@ const run = () => {
|
|
|
24
25
|
.option('--host [host]', '[string] specify hostname')
|
|
25
26
|
.option('--port <port>', '[number] specify port')
|
|
26
27
|
.option('--service-name <serviceName>', '[string] run a single service')
|
|
27
|
-
.action((...args) =>
|
|
28
|
+
.action(async (...args) => {
|
|
29
|
+
await rootTasksBootstrap('dev', ...args);
|
|
30
|
+
await serviceCommandBootstrap('dev', ...args);
|
|
31
|
+
});
|
|
28
32
|
|
|
29
33
|
cli
|
|
30
34
|
.command('build', 'Build project. Can supply a branding directory if running FERT standalone')
|
|
@@ -32,14 +36,20 @@ const run = () => {
|
|
|
32
36
|
.option('--config [dir]', 'Use custom rollup config file')
|
|
33
37
|
.option('--target <env>', 'Environment to build for, "dev" or "production"')
|
|
34
38
|
.option('--service-name <serviceName>', '[string] run a single service')
|
|
35
|
-
.action((...args) =>
|
|
39
|
+
.action(async (...args) => {
|
|
40
|
+
await rootTasksBootstrap('build', ...args);
|
|
41
|
+
await serviceCommandBootstrap('build', ...args);
|
|
42
|
+
});
|
|
36
43
|
|
|
37
44
|
cli
|
|
38
45
|
.command('publish', 'Publish the project')
|
|
39
46
|
.option('--target <env>', 'Environment to publish to, "dev" or "production"')
|
|
40
47
|
.option('--service-name <serviceName>', '[string] run a single service')
|
|
41
48
|
.option('--dry-run', 'Dry run, dont actually upload anything')
|
|
42
|
-
.action((...args) =>
|
|
49
|
+
.action(async (...args) => {
|
|
50
|
+
await rootTasksBootstrap('publish', ...args);
|
|
51
|
+
await serviceCommandBootstrap('publish', ...args);
|
|
52
|
+
});
|
|
43
53
|
|
|
44
54
|
cli
|
|
45
55
|
.command('configs', 'Query/Publish project configs')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import chokidar from 'chokidar5';
|
|
4
|
+
import { validateLocalConfigs } from '../utils/configs.js';
|
|
5
|
+
import { findFertConfigDir, loadConfigFromFile } from '../utils/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tasks to run against the root directory of a Branding repo (not a service folder).
|
|
9
|
+
*
|
|
10
|
+
* These are run once on a cli command
|
|
11
|
+
*
|
|
12
|
+
* @param {"dev"|"build"|"publish"} command
|
|
13
|
+
* @param {*} options
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
export async function rootTasksBootstrap(command /* , options */) {
|
|
17
|
+
const rootDir = await findFertConfigDir();
|
|
18
|
+
const configFromFile = await loadConfigFromFile({ dir: rootDir });
|
|
19
|
+
|
|
20
|
+
if (command === 'dev') {
|
|
21
|
+
await validateLocalConfigs({
|
|
22
|
+
workingDir: rootDir,
|
|
23
|
+
clientPropertyId: configFromFile.clientPropertyId,
|
|
24
|
+
throwable: false,
|
|
25
|
+
});
|
|
26
|
+
// watch local configs - revalidate on change
|
|
27
|
+
|
|
28
|
+
const configPaths = await Array.fromAsync(fs.promises.glob(path.resolve(rootDir, './config/*.json')));
|
|
29
|
+
chokidar.watch(configPaths, { ignoreInitial: true }).on('all', async () => {
|
|
30
|
+
await validateLocalConfigs({
|
|
31
|
+
workingDir: rootDir,
|
|
32
|
+
clientPropertyId: configFromFile.clientPropertyId,
|
|
33
|
+
throwable: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command === 'build') {
|
|
40
|
+
await validateLocalConfigs({
|
|
41
|
+
workingDir: rootDir,
|
|
42
|
+
clientPropertyId: configFromFile.clientPropertyId,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -15,6 +15,10 @@ const commandMap = {
|
|
|
15
15
|
/**
|
|
16
16
|
* determine if we are running a single service, otherwise search for all services
|
|
17
17
|
* Then run command
|
|
18
|
+
*
|
|
19
|
+
* @param {"dev"|"build"|"publish"} command
|
|
20
|
+
* @param {*} options
|
|
21
|
+
* @returns
|
|
18
22
|
*/
|
|
19
23
|
export async function serviceCommandBootstrap(command, options) {
|
|
20
24
|
// explicitly run a single service - via CLI option
|
|
@@ -64,9 +68,12 @@ export async function serviceCommandBootstrap(command, options) {
|
|
|
64
68
|
serviceName: serviceConfig.serviceName,
|
|
65
69
|
});
|
|
66
70
|
} catch (err) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Failed to run command ${chalk.green(command)} on service ${chalk.green(serviceConfig.serviceName)}: ${chalk.red(err.message)}`,
|
|
73
|
+
{
|
|
74
|
+
cause: err,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
}
|
package/bin/commands/build.js
CHANGED
|
@@ -5,14 +5,15 @@ import { log } from '../utils/logging.js';
|
|
|
5
5
|
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
|
-
import
|
|
9
|
-
import { runValidators, reportResults } from '../utils/validation-helpers.js';
|
|
10
|
-
import * as brandJsonValidator from '../validators/brand-json-validator.js';
|
|
11
|
-
|
|
12
|
-
const validators = [brandJsonValidator];
|
|
8
|
+
import * as validation from '../utils/validation.js';
|
|
13
9
|
|
|
14
10
|
export { bundleEntry, buildExternalAssets };
|
|
15
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Service-level task
|
|
14
|
+
* @param {*} options
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
16
17
|
export async function build(options = {}) {
|
|
17
18
|
const fertConfig = await resolveConfig(options);
|
|
18
19
|
const validTargets = ['dev', 'production'];
|
|
@@ -21,19 +22,7 @@ export async function build(options = {}) {
|
|
|
21
22
|
throw Error(`Missing or invalid --target option. Choose from [${validTargets}]`);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
await
|
|
25
|
-
workingDir: fertConfig.rootDir,
|
|
26
|
-
clientPropertyId: fertConfig.clientPropertyId,
|
|
27
|
-
});
|
|
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
|
-
}
|
|
25
|
+
await validation.runServiceValidators(['brand-json', 'translations'], { fertConfig, throwable: true });
|
|
37
26
|
|
|
38
27
|
if (!options.only) {
|
|
39
28
|
await rimraf(path.resolve(fertConfig.workingDir, 'dist'));
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import { glob } from 'node:fs/promises';
|
|
2
|
+
|
|
4
3
|
import chalk from 'chalk';
|
|
5
4
|
import open from 'open';
|
|
6
5
|
import chokidar from 'chokidar5';
|
|
7
|
-
import { resolveConfig
|
|
8
|
-
import
|
|
9
|
-
import { runValidators, reportResults } from '../utils/validation-helpers.js';
|
|
6
|
+
import { resolveConfig } from '../utils/index.js';
|
|
7
|
+
import * as validation from '../utils/validation.js';
|
|
10
8
|
import { devServer } from '../../server/server.js';
|
|
11
9
|
import { buildTokens, buildExternalAssets } from './build.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
BRAND_JSON_FILENAME,
|
|
12
|
+
FERT_CONFIG_FILENAME,
|
|
13
|
+
FERT_SERVICE_CONFIG_FILENAME,
|
|
14
|
+
SERVICE_TRANSLATIONS_FILEPATH,
|
|
15
|
+
} from '../../constants.js';
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Service-level task
|
|
19
|
+
* @param {*} options
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
15
22
|
export async function createDevServer(options = {}) {
|
|
16
23
|
let fertConfig = await resolveConfig(options);
|
|
17
24
|
|
|
@@ -21,36 +28,24 @@ export async function createDevServer(options = {}) {
|
|
|
21
28
|
)}\n`,
|
|
22
29
|
);
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
workingDir: fertConfig.rootDir,
|
|
26
|
-
clientPropertyId: fertConfig.clientPropertyId,
|
|
27
|
-
throwable: false,
|
|
28
|
-
});
|
|
29
|
-
await buildTokens(fertConfig);
|
|
30
|
-
|
|
31
|
-
const fertConfigDir = await findFertConfigDir();
|
|
32
|
-
|
|
33
|
-
// watch local configs - revalidate on change
|
|
34
|
-
const configPaths = await Array.fromAsync(glob(path.resolve(fertConfigDir, './config/*.json')));
|
|
35
|
-
chokidar.watch(configPaths, { ignoreInitial: true }).on('all', async () => {
|
|
36
|
-
await validateLocalConfigs({
|
|
37
|
-
workingDir: fertConfig.rootDir,
|
|
38
|
-
clientPropertyId: fertConfig.clientPropertyId,
|
|
39
|
-
throwable: false,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// watch brand.json & fert.config.js - refresh on change
|
|
31
|
+
// validation
|
|
44
32
|
const brandPath = path.resolve(fertConfig.workingDir, BRAND_JSON_FILENAME);
|
|
33
|
+
const translationPath = path.resolve(fertConfig.workingDir, SERVICE_TRANSLATIONS_FILEPATH);
|
|
34
|
+
// watch brand.json & translation.json - re-validate
|
|
35
|
+
chokidar.watch([brandPath, translationPath], { ignoreInitial: true }).on('all', async () => {
|
|
36
|
+
await validation.runServiceValidators(['brand-json', 'translations'], { fertConfig });
|
|
37
|
+
});
|
|
38
|
+
// initial validation run
|
|
39
|
+
await validation.runServiceValidators(['brand-json', 'translations'], { fertConfig });
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
// building tokens
|
|
42
|
+
await buildTokens(fertConfig);
|
|
43
|
+
// rebuild tokens for brand json update
|
|
44
|
+
chokidar.watch([brandPath]).on('all', async () => {
|
|
47
45
|
await buildTokens(fertConfig);
|
|
48
|
-
|
|
49
|
-
const results = await runValidators([brandJsonValidator], { rootDir: fertConfig.rootDir, options });
|
|
50
|
-
reportResults(results, rootDir);
|
|
51
46
|
});
|
|
52
47
|
|
|
53
|
-
const configPath = path.resolve(
|
|
48
|
+
const configPath = path.resolve(fertConfig.rootDir, FERT_CONFIG_FILENAME);
|
|
54
49
|
const serviceConfigPath = path.resolve(fertConfig.workingDir, FERT_SERVICE_CONFIG_FILENAME);
|
|
55
50
|
chokidar.watch([configPath, serviceConfigPath]).on('change', async () => {
|
|
56
51
|
console.log('Config changed, reloading…');
|
|
@@ -68,7 +63,8 @@ export async function createDevServer(options = {}) {
|
|
|
68
63
|
// start server
|
|
69
64
|
|
|
70
65
|
/**
|
|
71
|
-
* having a port number to start with helps keep the ports changing too much in development
|
|
66
|
+
* having a port number to start with helps keep the ports changing too much in development.
|
|
67
|
+
* This is the exception to the rule, do not create map-like objects for hardcoded service names.
|
|
72
68
|
*/
|
|
73
69
|
const STARTING_PORT_FOR_SERVICE_FOR_CONVENIENCE = {
|
|
74
70
|
'jobseekers-frontend': 4001,
|
package/bin/commands/publish.js
CHANGED
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
ASSET_STORE_INVALIDATION_PATH,
|
|
18
18
|
} from '../../constants.js';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Service-level task
|
|
22
|
+
* @param {*} options
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
20
25
|
export async function publish(options) {
|
|
21
26
|
const fertConfig = await resolveConfig(options);
|
|
22
27
|
const validTargets = ['dev', 'prod', 'production'];
|
package/bin/commands/validate.js
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { loadServiceConfigFiles, resolveConfig } from '../utils/index.js';
|
|
2
|
+
import * as validation from '../utils/validation.js';
|
|
3
3
|
import { log } from '../utils/logging.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function validateCommand(options) {
|
|
5
|
+
/**
|
|
6
|
+
* root-level task
|
|
7
|
+
* @param {*} options
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export async function validateCommand(/* options */) {
|
|
11
11
|
log.info('Running validations…\n');
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
options,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const results = await runValidators(validators, context);
|
|
21
|
-
const exitCode = reportResults(results, rootDir);
|
|
13
|
+
const serviceConfigs = await loadServiceConfigFiles();
|
|
14
|
+
// run validators for each service
|
|
15
|
+
for (const { serviceConfig } of serviceConfigs) {
|
|
16
|
+
const fertConfig = await resolveConfig({ serviceName: serviceConfig.serviceName });
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
await validation.runServiceValidators(['brand-json', 'translations'], {
|
|
19
|
+
fertConfig,
|
|
20
|
+
throwable: true,
|
|
21
|
+
});
|
|
26
22
|
}
|
|
27
23
|
}
|
package/bin/utils/cpid-lookup.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PersistentCacheWithTtl } from './persistent-cache-with-ttl.js';
|
|
|
6
6
|
const cache = new PersistentCacheWithTtl('cpid-cache', {
|
|
7
7
|
ttl: ONE_WEEK,
|
|
8
8
|
});
|
|
9
|
-
|
|
9
|
+
/** @return {Promise<Client>} */
|
|
10
10
|
export async function doCpidLookup(clientPropertyId) {
|
|
11
11
|
const API_URL = new URL(clientPropertyId, PROPERTY_ID_API).toString();
|
|
12
12
|
try {
|
|
@@ -33,7 +33,7 @@ export async function doCpidLookup(clientPropertyId) {
|
|
|
33
33
|
throw error;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** @return {Promise<Client>} */
|
|
37
37
|
export async function cpidLookup(clientPropertyId, options = {}) {
|
|
38
38
|
if (options.purgeCache) {
|
|
39
39
|
cache.purgeCacheFile();
|
package/bin/utils/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import { glob } from 'node:fs/promises';
|
|
6
6
|
import { findUp } from 'find-up-simple';
|
|
7
7
|
import chalk from 'chalk';
|
|
@@ -18,13 +18,20 @@ export function printBanner() {
|
|
|
18
18
|
console.log(`\n${chalk.green.bold('Fert')} v${VERSION}`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* get a config values from `clientconfig` from config-api
|
|
23
|
+
*
|
|
24
|
+
* @param {string} cpid
|
|
25
|
+
* @param {Array<string>} configNames array of config keys to fetch from clientconfig
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
export async function getClientConfig(cpid, configNames = []) {
|
|
22
29
|
Hoek.assert(Array.isArray(configNames), 'configNames must be an array');
|
|
23
30
|
|
|
24
31
|
const result = {};
|
|
25
32
|
|
|
26
33
|
for (let config of configNames) {
|
|
27
|
-
const url = CONFIG_API.replace('{cpid}', cpid).replace('{config}', config);
|
|
34
|
+
const url = CONFIG_API.replace('{configGroup}', 'clientconfig').replace('{cpid}', cpid).replace('{config}', config);
|
|
28
35
|
const { data } = await fetch(url).then((response) => response.json());
|
|
29
36
|
const { value } = data?.[0] || {};
|
|
30
37
|
|
|
@@ -89,7 +96,7 @@ export async function resolveConfig(options = {}) {
|
|
|
89
96
|
|
|
90
97
|
fertConfig.externalAssets = resolveExternalAssets(fertConfig.externalAssets);
|
|
91
98
|
|
|
92
|
-
fertConfig.config = await
|
|
99
|
+
fertConfig.config = await getClientConfig(fertConfig.clientPropertyId, ['JobseekerSiteWebSitePath', 'SiteName']);
|
|
93
100
|
|
|
94
101
|
return {
|
|
95
102
|
...fertConfig,
|
|
@@ -117,7 +124,7 @@ export async function findFertConfigDir() {
|
|
|
117
124
|
/**
|
|
118
125
|
* find and load all service config files, return config and location
|
|
119
126
|
* @param {string} root directory path
|
|
120
|
-
* @returns
|
|
127
|
+
* @returns {Array<{dir:string, serviceConfig:FertServiceConfigFile}>}
|
|
121
128
|
*/
|
|
122
129
|
export async function loadServiceConfigFiles() {
|
|
123
130
|
const rootDir = await findFertConfigDir();
|
|
@@ -141,6 +148,14 @@ export async function loadServiceConfigFiles() {
|
|
|
141
148
|
return serviceConfigs;
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
/**
|
|
152
|
+
* load FERT JSON file from a directory, optionally specifying the config files name.
|
|
153
|
+
* @param {object} options
|
|
154
|
+
* @param {string} [options.dir] defaults to process.cwd()
|
|
155
|
+
* @param {string} options.filename default FERT_CONFIG_FILENAME, e.g. use `FERT_SERVICE_CONFIG_FILENAME` to load service-level config file
|
|
156
|
+
* @param {boolean} options.silent set to `true` to not throw if file cant be loaded
|
|
157
|
+
* @returns {FertConfigFile | FertServiceConfigFile} depending on filename provided
|
|
158
|
+
*/
|
|
144
159
|
export async function loadConfigFromFile({ dir = process.cwd(), filename = FERT_CONFIG_FILENAME, silent = false }) {
|
|
145
160
|
const configPath = path.resolve(dir, filename);
|
|
146
161
|
try {
|
|
@@ -166,7 +181,7 @@ export function ensureTrailingSlash(str) {
|
|
|
166
181
|
|
|
167
182
|
export async function exists(_path) {
|
|
168
183
|
try {
|
|
169
|
-
fs.access(_path);
|
|
184
|
+
await fs.promises.access(_path);
|
|
170
185
|
return true;
|
|
171
186
|
} catch {
|
|
172
187
|
return false;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { log } from './logging.js';
|
|
3
|
+
import * as validatorBrandJson from '../validators/brand-json.validator.js';
|
|
4
|
+
import * as validatorTranslations from '../validators/translations.validator.js';
|
|
5
|
+
|
|
6
|
+
/** @type {Array<Validator>} */
|
|
7
|
+
const validators = [validatorBrandJson, validatorTranslations];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* Runs an array of validator functions and collects all results. This is targeting a service, and requires a fertConfig.
|
|
12
|
+
* Each validator receives `{fertConfig}` and must return an array of result objects.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<string>} validatorNames, array of strings matching name from validator js file. e.g. ['brand-json', 'translations']
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {FertConfig} options.fertConfig required
|
|
17
|
+
* @param {object} options.throwable to throw if validation has errors
|
|
18
|
+
* @returns {Array<ValidationResult>}
|
|
19
|
+
*/
|
|
20
|
+
export async function runServiceValidators(validatorNames = [], { fertConfig, throwable = false } = {}) {
|
|
21
|
+
const results = [];
|
|
22
|
+
|
|
23
|
+
for (const validator of validators) {
|
|
24
|
+
if (!validatorNames.includes(validator.name)) continue;
|
|
25
|
+
const validatorResults = await validator.validate({ fertConfig });
|
|
26
|
+
results.push(...validatorResults);
|
|
27
|
+
}
|
|
28
|
+
const validationExitCode = reportResults(results, fertConfig);
|
|
29
|
+
if (throwable && validationExitCode !== 0) {
|
|
30
|
+
throw new Error('Validation has errors');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
/** @type {ValidationSeverity} */
|
|
36
|
+
const Severity = Object.freeze({
|
|
37
|
+
ERROR: 'ERROR',
|
|
38
|
+
WARNING: 'WARNING',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns {ValidationResultCollection}
|
|
43
|
+
*/
|
|
44
|
+
export function createResultCollection() {
|
|
45
|
+
/** @type {Array<ValidationResult>} */
|
|
46
|
+
const results = [];
|
|
47
|
+
/** @type {ValidationResultCollection} */
|
|
48
|
+
return {
|
|
49
|
+
results,
|
|
50
|
+
/** add a result to results collection, and returns a builder to modify the results severity, filePath and detail */
|
|
51
|
+
addResult(message) {
|
|
52
|
+
/** @type {ValidationResult} */
|
|
53
|
+
const result = { severity: Severity.WARNING, message, filePath: undefined, detail: undefined };
|
|
54
|
+
results.push(result);
|
|
55
|
+
/** @type {ValidationResultBuilder} */
|
|
56
|
+
return {
|
|
57
|
+
/** set ValidationResult.severity to ERROR */
|
|
58
|
+
err() {
|
|
59
|
+
result.severity = Severity.ERROR;
|
|
60
|
+
return this;
|
|
61
|
+
},
|
|
62
|
+
/** set ValidationResult.filePath */
|
|
63
|
+
filePath(_filePath) {
|
|
64
|
+
result.filePath = _filePath;
|
|
65
|
+
return this;
|
|
66
|
+
},
|
|
67
|
+
/** set ValidationResult.filePath */
|
|
68
|
+
detail(_detail) {
|
|
69
|
+
result.detail = _detail;
|
|
70
|
+
return this;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Prints validation results — errors first, then warnings grouped by file.
|
|
79
|
+
* Full paths are shown.
|
|
80
|
+
* Returns 1 if any ERROR results exist, 0 otherwise.
|
|
81
|
+
*
|
|
82
|
+
* @param {Array<ValidationResult>} results
|
|
83
|
+
* @param {FertConfig} fertConfig
|
|
84
|
+
* @returns {0|1}
|
|
85
|
+
*/
|
|
86
|
+
export function reportResults(results, fertConfig) {
|
|
87
|
+
const errors = results.filter((r) => r.severity === Severity.ERROR);
|
|
88
|
+
if (errors.length) {
|
|
89
|
+
console.log(chalk.red.bold(`\n✖ ${errors.length} error${errors.length === 1 ? '' : 's'}\n`));
|
|
90
|
+
logResults(errors);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const warnings = results.filter((r) => r.severity === Severity.WARNING);
|
|
94
|
+
if (warnings.length) {
|
|
95
|
+
console.log(chalk.yellow.bold(`\n⚠ ${warnings.length} warning${warnings.length === 1 ? '' : 's'}\n`));
|
|
96
|
+
logResults(warnings);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!errors.length && !warnings.length) {
|
|
100
|
+
log.success(`All validations passed for ${chalk.green(fertConfig.workingDir)}`);
|
|
101
|
+
} else {
|
|
102
|
+
log.warn(`all validations did not passed for ${chalk.yellow(fertConfig.workingDir)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return errors.length > 0 ? 1 : 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Group errors by filePath
|
|
110
|
+
* @param {Array<ValidationResult>} results
|
|
111
|
+
* @returns {Map<string, Array<ValidationResult>>}
|
|
112
|
+
*/
|
|
113
|
+
function groupByFilePath(results) {
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const result of results) {
|
|
116
|
+
const key = result.filePath ?? 'no-file-path';
|
|
117
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
118
|
+
groups.get(key).push(result);
|
|
119
|
+
}
|
|
120
|
+
return groups;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log each ValidationResult in the array
|
|
125
|
+
* @param {Array<ValidationResult>} results
|
|
126
|
+
*/
|
|
127
|
+
function logResults(results) {
|
|
128
|
+
// order results, and only print filePath once per file
|
|
129
|
+
const groups = groupByFilePath(results);
|
|
130
|
+
for (const [filePath, items] of groups) {
|
|
131
|
+
if (filePath) {
|
|
132
|
+
console.log(chalk.dim(`${filePath}`));
|
|
133
|
+
}
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const color = item.severity === Severity.ERROR ? chalk.red : chalk.yellow;
|
|
136
|
+
const symbol = item.severity === Severity.ERROR ? '✖' : '⚠';
|
|
137
|
+
const indent = filePath ? ' ' : ' ';
|
|
138
|
+
console.log(color.bold(symbol), `${indent}${item.message}`);
|
|
139
|
+
if (item.detail) {
|
|
140
|
+
console.log(chalk.dim(` ${indent}${item.detail}`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log('\n');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -3,9 +3,9 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { createStyleDictionary } from '@madgex/design-system/style-dictionary';
|
|
4
4
|
// eslint-disable-next-line n/no-extraneous-import
|
|
5
5
|
import { resolveReferences } from 'style-dictionary/utils';
|
|
6
|
-
import
|
|
7
|
-
import { Severity, createResult } from '../utils/validation-helpers.js';
|
|
6
|
+
import * as validation from '../utils/validation.js';
|
|
8
7
|
import { BRAND_JSON_FILENAME } from '../../constants.js';
|
|
8
|
+
import { exists } from '../utils/index.js';
|
|
9
9
|
|
|
10
10
|
export const name = 'brand-json';
|
|
11
11
|
export const description = 'Validates brand.json overrides against the MDS base token schema';
|
|
@@ -45,9 +45,9 @@ export function extractTokenPaths(obj, prefix = '') {
|
|
|
45
45
|
*/
|
|
46
46
|
export function resolveToFinalValue(rawValue, tokens) {
|
|
47
47
|
if (typeof rawValue !== 'string' || !rawValue.includes('{')) return rawValue;
|
|
48
|
-
const resolved = resolveReferences(rawValue, tokens
|
|
49
|
-
if (resolved) {
|
|
50
|
-
return resolveToFinalValue(resolved, tokens);
|
|
48
|
+
const resolved = resolveReferences(rawValue, tokens);
|
|
49
|
+
if (resolved !== undefined && typeof resolved === 'object' && '$value' in resolved) {
|
|
50
|
+
return resolveToFinalValue(resolved.$value, tokens, { usesDtcg: true });
|
|
51
51
|
}
|
|
52
52
|
return resolved;
|
|
53
53
|
}
|
|
@@ -83,78 +83,64 @@ async function loadBaseSchema() {
|
|
|
83
83
|
await cleanTempFiles();
|
|
84
84
|
return { basePaths, baseResolvedValues, baseTokens };
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Main validation function.
|
|
88
|
+
* Validates brand json override file for specified service (fertConfig)
|
|
89
|
+
* @property {object} options
|
|
90
|
+
* @property {FertConfig} options.fertConfig
|
|
91
|
+
* @returns {Promise<Array<ValidationResult>>} Array of validation results
|
|
92
|
+
*/
|
|
93
|
+
export async function validate({ fertConfig }) {
|
|
94
|
+
const resultsCollection = validation.createResultCollection();
|
|
89
95
|
const { basePaths, baseResolvedValues, baseTokens } = await loadBaseSchema();
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
const brandJsonPath = path.join(fertConfig.workingDir, BRAND_JSON_FILENAME);
|
|
98
|
+
|
|
99
|
+
if (!(await exists(brandJsonPath))) {
|
|
100
|
+
resultsCollection
|
|
101
|
+
.addResult(`Missing ${BRAND_JSON_FILENAME}`)
|
|
102
|
+
.filePath(brandJsonPath)
|
|
103
|
+
.detail('Each service should have a brand.json for token overrides');
|
|
104
|
+
return resultsCollection.results; // abort early
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let brandData;
|
|
92
108
|
try {
|
|
93
|
-
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return results;
|
|
109
|
+
brandData = JSON.parse(fs.readFileSync(brandJsonPath, 'utf-8'));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
resultsCollection
|
|
112
|
+
.addResult(`Invalid JSON in ${BRAND_JSON_FILENAME}`)
|
|
113
|
+
.err()
|
|
114
|
+
.filePath(brandJsonPath)
|
|
115
|
+
.detail(err.message);
|
|
116
|
+
return resultsCollection.results; // abort early
|
|
101
117
|
}
|
|
102
118
|
|
|
103
|
-
|
|
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
|
-
}
|
|
119
|
+
const overridePaths = extractTokenPaths(brandData);
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
filePath: brandJsonPath,
|
|
123
|
-
detail: err.message,
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
continue;
|
|
121
|
+
for (const overridePath of overridePaths) {
|
|
122
|
+
if (!basePaths.has(overridePath)) {
|
|
123
|
+
resultsCollection
|
|
124
|
+
.addResult(`Unknown token path: "${overridePath}"`)
|
|
125
|
+
.filePath(brandJsonPath)
|
|
126
|
+
.detail('This path does not exist in the MDS base schema and will have no effect');
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const
|
|
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
|
-
|
|
129
|
+
const rawOverrideValue = getValueAtPath(brandData, overridePath);
|
|
130
|
+
if (rawOverrideValue !== undefined) {
|
|
145
131
|
const resolvedOverride = resolveToFinalValue(rawOverrideValue, baseTokens);
|
|
146
132
|
const resolvedBase = baseResolvedValues.get(overridePath);
|
|
147
133
|
|
|
148
134
|
if (resolvedOverride === resolvedBase) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
135
|
+
resultsCollection
|
|
136
|
+
.addResult(`Redundant override: "${overridePath}"`)
|
|
137
|
+
.filePath(brandJsonPath)
|
|
138
|
+
.detail(
|
|
139
|
+
`Value "${String(rawOverrideValue)}" resolves to the same value as the MDS base ("${String(resolvedBase)}")`,
|
|
140
|
+
);
|
|
155
141
|
}
|
|
156
142
|
}
|
|
157
143
|
}
|
|
158
144
|
|
|
159
|
-
return results;
|
|
145
|
+
return resultsCollection.results;
|
|
160
146
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import * as validation from '../utils/validation.js';
|
|
4
|
+
import { PersistentCacheWithTtl } from '../utils/persistent-cache-with-ttl.js';
|
|
5
|
+
import { ONE_WEEK, SERVICE_API_TRANSLATIONS, SERVICE_TRANSLATIONS_FILEPATH } from '../../constants.js';
|
|
6
|
+
import { exists } from '../utils/index.js';
|
|
7
|
+
import { log } from '../utils/logging.js';
|
|
8
|
+
|
|
9
|
+
export const name = 'translations';
|
|
10
|
+
export const description = 'Validates translation override files against base translations from upstream repositories';
|
|
11
|
+
|
|
12
|
+
const cache = new PersistentCacheWithTtl('base-translations', { ttl: ONE_WEEK });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetches base translations from remote version of this service.
|
|
16
|
+
* Uses caching with 1-week TTL to minimize API calls while dev server is running.
|
|
17
|
+
*
|
|
18
|
+
* @property {FertConfig} fertConfig
|
|
19
|
+
* @returns {Promise<Object|null>} Base translations object, or null if fetch fails
|
|
20
|
+
*/
|
|
21
|
+
async function fetchBaseTranslations(fertConfig) {
|
|
22
|
+
const cacheKey = `translations-${fertConfig.serviceName}-${fertConfig.clientPropertyId}`;
|
|
23
|
+
// Check cache first
|
|
24
|
+
const cached = cache.get(cacheKey);
|
|
25
|
+
if (cached) return cached;
|
|
26
|
+
const url = new URL(SERVICE_API_TRANSLATIONS.replace('{serviceName}', fertConfig.serviceName));
|
|
27
|
+
// let service endpoint know we dont want to include client translations overrides, that's what we're looking at in our branding repo!
|
|
28
|
+
url.searchParams.set('excludeClientTranslationOverrides', true);
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url.href, {
|
|
31
|
+
headers: { 'x-client-property-id': fertConfig.clientPropertyId },
|
|
32
|
+
signal: AbortSignal.timeout(10000),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
throw new Error(`failed to fetch base translations - ${text}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const translations = await res.json();
|
|
41
|
+
cache.set(cacheKey, translations);
|
|
42
|
+
return translations;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recursively flattens a nested translation object into a Map of key paths to values.
|
|
47
|
+
* Example: { header: { "sign-in": "Sign in" } } → Map { "header.sign-in" => "Sign in" }
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} obj - Translation object to flatten
|
|
50
|
+
* @param {string} prefix - Current path prefix
|
|
51
|
+
* @returns {Map<string, any>} Map of flattened key paths to their values
|
|
52
|
+
*/
|
|
53
|
+
export function flattenKeysWithValues(obj, prefix = '') {
|
|
54
|
+
const entries = new Map();
|
|
55
|
+
|
|
56
|
+
if (!obj || typeof obj !== 'object') {
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
61
|
+
const currentPath = prefix ? `${prefix}.${key}` : key;
|
|
62
|
+
|
|
63
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
64
|
+
// Recurse into nested objects
|
|
65
|
+
const childEntries = flattenKeysWithValues(value, currentPath);
|
|
66
|
+
for (const [childKey, childValue] of childEntries) {
|
|
67
|
+
entries.set(childKey, childValue);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Leaf node - add the path with its value
|
|
71
|
+
entries.set(currentPath, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return entries;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Main validation function.
|
|
79
|
+
* Validates translation override file for specified service (fertConfig)
|
|
80
|
+
* @property {object} options
|
|
81
|
+
* @property {FertConfig} options.fertConfig
|
|
82
|
+
* @returns {Promise<Array<ValidationResult>>} Array of validation results
|
|
83
|
+
*/
|
|
84
|
+
export async function validate({ fertConfig }) {
|
|
85
|
+
const resultsCollection = validation.createResultCollection();
|
|
86
|
+
|
|
87
|
+
const overridePath = path.resolve(fertConfig.workingDir, SERVICE_TRANSLATIONS_FILEPATH);
|
|
88
|
+
// cant read override file - nothing to validate
|
|
89
|
+
if (!(await exists(overridePath))) return resultsCollection.results; // abort early
|
|
90
|
+
|
|
91
|
+
// Parse override file (not using `import` to not cache json file as a module)
|
|
92
|
+
let overrideTranslations;
|
|
93
|
+
try {
|
|
94
|
+
const overrideContent = await fs.promises.readFile(overridePath, 'utf-8');
|
|
95
|
+
overrideTranslations = JSON.parse(overrideContent);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
resultsCollection.addResult(`Failed to parse translation override: ${err.message}`).err().filePath(overridePath);
|
|
98
|
+
return resultsCollection.results; // abort early
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fetch remote base translations
|
|
102
|
+
let baseTranslations;
|
|
103
|
+
try {
|
|
104
|
+
baseTranslations = await fetchBaseTranslations(fertConfig);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
log.warn(`Could not fetch base translations for ${fertConfig.serviceName}. Skipping validation. ${err.message}`);
|
|
107
|
+
|
|
108
|
+
return resultsCollection.results; // abort early
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Flatten both translation objects with values
|
|
112
|
+
const baseEntries = flattenKeysWithValues(baseTranslations);
|
|
113
|
+
const overrideEntries = flattenKeysWithValues(overrideTranslations);
|
|
114
|
+
|
|
115
|
+
// Check each override key
|
|
116
|
+
for (const [overrideKey, overrideValue] of overrideEntries) {
|
|
117
|
+
if (!baseEntries.has(overrideKey)) {
|
|
118
|
+
// Extra key that doesn't exist in base
|
|
119
|
+
resultsCollection
|
|
120
|
+
.addResult(
|
|
121
|
+
`Unknown key: Translation key "${overrideKey}" is defined in override but does not exist in base translations`,
|
|
122
|
+
)
|
|
123
|
+
.filePath(overridePath)
|
|
124
|
+
.detail(
|
|
125
|
+
`This key may be unused or invalid. Base translations for ${fertConfig.serviceName} do not include this key.`,
|
|
126
|
+
);
|
|
127
|
+
} else if (baseEntries.get(overrideKey) === overrideValue) {
|
|
128
|
+
// Redundant override with same value as base
|
|
129
|
+
resultsCollection
|
|
130
|
+
.addResult(`Redundant override: "${overrideKey}"`)
|
|
131
|
+
.filePath(overridePath)
|
|
132
|
+
.detail(`Value "${String(overrideValue)}" is the same as the base translation and can be removed`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return resultsCollection.results;
|
|
137
|
+
}
|
package/constants.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import os from 'node:os';
|
|
4
|
-
import
|
|
5
|
+
import pkg from './package.json' with { type: 'json' };
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
export const { version: VERSION } = JSON.parse(
|
|
9
|
-
fs.readFileSync(path.resolve(import.meta.dirname, './package.json')).toString(),
|
|
10
|
-
);
|
|
7
|
+
export const VERSION = pkg.version;
|
|
11
8
|
|
|
12
9
|
export const ONE_MINUTE = 60000;
|
|
13
10
|
export const ONE_HOUR = ONE_MINUTE * 60;
|
|
@@ -17,7 +14,7 @@ export const ONE_WEEK = ONE_DAY * 7;
|
|
|
17
14
|
export const TMP_DIR = fs.realpathSync(os.tmpdir());
|
|
18
15
|
export const FERT_PACKAGE_DIR = path.resolve('./');
|
|
19
16
|
export const CUSTOM_TEMPLATES_DIR = './pages';
|
|
20
|
-
export const MDS_PATH = path.dirname(
|
|
17
|
+
export const MDS_PATH = path.dirname(fileURLToPath(import.meta.resolve('@madgex/design-system/package.json')));
|
|
21
18
|
export const AWS_REGION = 'eu-west-1';
|
|
22
19
|
export const PROPERTY_ID_API = 'https://property-identification-api.cs.madgexhosting.net/properties/';
|
|
23
20
|
export const ASSETS_API_URL = 'https://asset-store.job.madgexhosting.net';
|
|
@@ -50,9 +47,14 @@ export const ASSET_STORE_INVALIDATION_PATH = `/{fertConfig.client.rootClientProp
|
|
|
50
47
|
export const ASSET_STORE_USER_GUID = 'a386d4b6-f2df-4b80-ad1f-0349e23f530b';
|
|
51
48
|
|
|
52
49
|
export const CONFIG_API =
|
|
53
|
-
'https://configuration-api.job.madgexhosting.net/configs/override/{cpid}/production/
|
|
50
|
+
'https://configuration-api.job.madgexhosting.net/configs/override/{cpid}/production/{configGroup}/{config}';
|
|
54
51
|
export const CONFIG_DIR = 'config';
|
|
55
52
|
|
|
53
|
+
/** we assume the remove version of a service hosts an api translations endpoint */
|
|
54
|
+
export const SERVICE_API_TRANSLATIONS = 'https://{serviceName}.job.madgexhosting.net/api/translations';
|
|
55
|
+
|
|
56
|
+
export const SERVICE_TRANSLATIONS_FILEPATH = 'public/translation.json';
|
|
57
|
+
|
|
56
58
|
export const MOCK_AUTH_OBJECT = {
|
|
57
59
|
'recruiterservices-frontend': {
|
|
58
60
|
credentials: {
|
package/jsconfig.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madgex/fert",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "Tool to help build the V6 branding",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fert": "./bin/cli.js"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"url": "https://github.com/wiley/madgex-frontend-rollout-tool.git"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=22.
|
|
22
|
+
"node": ">=22.17"
|
|
23
23
|
},
|
|
24
24
|
"author": "Madgex",
|
|
25
25
|
"license": "UNLICENSED",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"simple-git": "3.33.0",
|
|
52
52
|
"simple-update-notifier": "2.0.0",
|
|
53
53
|
"uuid-validate": "0.0.3",
|
|
54
|
-
"vite": "7.3.
|
|
54
|
+
"vite": "7.3.2",
|
|
55
55
|
"vite-plugin-static-copy": "3.4.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"eslint": "10.1.0",
|
|
63
63
|
"husky": "9.1.7",
|
|
64
64
|
"lint-staged": "16.4.0",
|
|
65
|
-
"msw": "
|
|
65
|
+
"msw": "2.13.0",
|
|
66
66
|
"prettier": "3.8.1",
|
|
67
67
|
"semantic-release": "25.0.3"
|
|
68
68
|
},
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type ValidationSeverity = {
|
|
2
|
+
ERROR: 'ERROR';
|
|
3
|
+
WARNING: 'WARNING';
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type Validator = {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
validate: function;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ValidationResult = {
|
|
13
|
+
severity: ValidationSeverity;
|
|
14
|
+
message: string;
|
|
15
|
+
filePath: string;
|
|
16
|
+
detail: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ValidationResultCollection = {
|
|
20
|
+
results: ValidationResult[];
|
|
21
|
+
addResult(message: string): ValidationResultBuilder;
|
|
22
|
+
};
|
|
23
|
+
type ValidationResultBuilder = {
|
|
24
|
+
err(): ValidationResultBuilder;
|
|
25
|
+
filePath(filePath: string): ValidationResultBuilder;
|
|
26
|
+
detail(detail: string): ValidationResultBuilder;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface FertConfigFile {
|
|
30
|
+
clientPropertyId: string;
|
|
31
|
+
}
|
|
32
|
+
interface FertServiceConfigFile {
|
|
33
|
+
serviceName: string;
|
|
34
|
+
entry?: string;
|
|
35
|
+
externalAssets?: {
|
|
36
|
+
links?: string[] | string;
|
|
37
|
+
scripts?: string[] | string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
type Client = {
|
|
41
|
+
brandName: string;
|
|
42
|
+
parentCpid: string;
|
|
43
|
+
rootClientPropertyId: string;
|
|
44
|
+
isAffiliate: boolean;
|
|
45
|
+
};
|
|
46
|
+
type FertConfig = FertConfigFile &
|
|
47
|
+
FertServiceConfigFile & {
|
|
48
|
+
/** always the root of the branding repo where fert.config.js file is */
|
|
49
|
+
rootDir: string;
|
|
50
|
+
/** always the service directory where fert.service.config.js is */
|
|
51
|
+
workingDir: any;
|
|
52
|
+
cli: object;
|
|
53
|
+
client: Client;
|
|
54
|
+
};
|
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
}
|