@madgex/fert 1.3.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/.eslintignore +8 -0
- package/.eslintrc.js +15 -0
- package/.github/workflows/commits.yml +14 -0
- package/.github/workflows/release.yml +29 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.prettierignore +2 -0
- package/.prettierrc.js +8 -0
- package/README.md +232 -0
- package/bin/cli.js +68 -0
- package/bin/commands/build-tasks/build-external-assets.js +16 -0
- package/bin/commands/build-tasks/build-tokens.js +119 -0
- package/bin/commands/build-tasks/bundle-ds-css.js +74 -0
- package/bin/commands/build-tasks/bundle-entry.js +43 -0
- package/bin/commands/build.js +42 -0
- package/bin/commands/dev-server.js +50 -0
- package/bin/commands/init.js +207 -0
- package/bin/commands/publish-tasks/asset-store-uploader.js +94 -0
- package/bin/commands/publish-tasks/get-aws-parameter.js +41 -0
- package/bin/commands/publish.js +51 -0
- package/bin/utils/cpid-lookup.js +47 -0
- package/bin/utils/index.js +121 -0
- package/bin/utils/resolve-external-assets.js +77 -0
- package/commitlint.config.cjs +1 -0
- package/constants.js +36 -0
- package/delivery/git.cer +22 -0
- package/delivery/jenkinsfile +93 -0
- package/package.json +84 -0
- package/repo-templates/globals/fert.config.js +4 -0
- package/repo-templates/globals/jenkinsfile +4 -0
- package/repo-templates/template-basic/_gitignore +5 -0
- package/repo-templates/template-basic/brand.json +21 -0
- package/repo-templates/template-basic/package.json +13 -0
- package/repo-templates/template-basic/public/favicon.ico +0 -0
- package/repo-templates/template-basic/public/fonts/mdgx-icons.eot +0 -0
- package/repo-templates/template-basic/public/fonts/mdgx-icons.svg +71 -0
- package/repo-templates/template-basic/public/fonts/mdgx-icons.ttf +0 -0
- package/repo-templates/template-basic/public/fonts/mdgx-icons.woff +0 -0
- package/repo-templates/template-basic/public/fonts/my-font.woff +1 -0
- package/repo-templates/template-basic/public/images/logo.png +0 -0
- package/repo-templates/template-basic/src/css/styles.scss +12 -0
- package/repo-templates/template-basic/src/index.js +3 -0
- package/repo-templates/template-basic/templates/footer.njk +14 -0
- package/repo-templates/template-basic/templates/header.njk +14 -0
- package/repo-templates/template-bigworkbag/_gitignore +5 -0
- package/repo-templates/template-bigworkbag/brand.json +21 -0
- package/repo-templates/template-bigworkbag/package.json +13 -0
- package/repo-templates/template-bigworkbag/public/favicon.ico +0 -0
- package/repo-templates/template-bigworkbag/public/fonts/mdgx-icons.eot +0 -0
- package/repo-templates/template-bigworkbag/public/fonts/mdgx-icons.svg +71 -0
- package/repo-templates/template-bigworkbag/public/fonts/mdgx-icons.ttf +0 -0
- package/repo-templates/template-bigworkbag/public/fonts/mdgx-icons.woff +0 -0
- package/repo-templates/template-bigworkbag/public/fonts/my-font.woff +1 -0
- package/repo-templates/template-bigworkbag/public/images/logo.png +0 -0
- package/repo-templates/template-bigworkbag/public/images/user-menu-pointer.svg +4 -0
- package/repo-templates/template-bigworkbag/src/css/breakpoints.scss +17 -0
- package/repo-templates/template-bigworkbag/src/css/clicky-menu.scss +52 -0
- package/repo-templates/template-bigworkbag/src/css/desktop-main-nav.scss +25 -0
- package/repo-templates/template-bigworkbag/src/css/desktop-user-nav.scss +127 -0
- package/repo-templates/template-bigworkbag/src/css/footer.scss +77 -0
- package/repo-templates/template-bigworkbag/src/css/header-top.scss +29 -0
- package/repo-templates/template-bigworkbag/src/css/leaderboard-ad.scss +22 -0
- package/repo-templates/template-bigworkbag/src/css/mobile-main-nav.scss +68 -0
- package/repo-templates/template-bigworkbag/src/css/mobile-user-nav.scss +49 -0
- package/repo-templates/template-bigworkbag/src/css/reset.scss +91 -0
- package/repo-templates/template-bigworkbag/src/css/styles.scss +19 -0
- package/repo-templates/template-bigworkbag/src/css/top-bar.scss +13 -0
- package/repo-templates/template-bigworkbag/src/css/typography.scss +20 -0
- package/repo-templates/template-bigworkbag/src/css/util.scss +28 -0
- package/repo-templates/template-bigworkbag/src/css/variables.scss +9 -0
- package/repo-templates/template-bigworkbag/src/index.js +3 -0
- package/repo-templates/template-bigworkbag/src/js/clicky-menus.js +178 -0
- package/repo-templates/template-bigworkbag/src/js/no-js.js +10 -0
- package/repo-templates/template-bigworkbag/templates/context/footer-nav.njk +27 -0
- package/repo-templates/template-bigworkbag/templates/context/main-nav.njk +32 -0
- package/repo-templates/template-bigworkbag/templates/context/user-nav.njk +17 -0
- package/repo-templates/template-bigworkbag/templates/footer.njk +68 -0
- package/repo-templates/template-bigworkbag/templates/header.njk +54 -0
- package/repo-templates/template-bigworkbag/templates/includes/desktop-main-nav-items.njk +18 -0
- package/repo-templates/template-bigworkbag/templates/includes/desktop-user-nav/loggedin-items.njk +28 -0
- package/repo-templates/template-bigworkbag/templates/includes/desktop-user-nav/loggedout-items.njk +9 -0
- package/repo-templates/template-bigworkbag/templates/includes/desktop-user-nav/submenu-items.njk +18 -0
- package/repo-templates/template-bigworkbag/templates/includes/desktop-user-nav/user-nav.njk +24 -0
- package/repo-templates/template-bigworkbag/templates/includes/dev-console-context.njk +7 -0
- package/repo-templates/template-bigworkbag/templates/includes/mobile-main-nav-items.njk +25 -0
- package/repo-templates/template-bigworkbag/templates/includes/mobile-user-nav/loggedin-items.njk +33 -0
- package/repo-templates/template-bigworkbag/templates/includes/mobile-user-nav/loggedout-items.njk +16 -0
- package/repo-templates/template-bigworkbag/templates/includes/mobile-user-nav/user-nav.njk +24 -0
- package/repo-templates/template-bigworkbag/templates/svgs/arrow-down.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/arrow-left.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/arrow-right.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/arrow-up.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/cart.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/facebook.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/linkedin.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/profile.svg +5 -0
- package/repo-templates/template-bigworkbag/templates/svgs/star-filled.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/star-outline.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/twitter.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/svgs/youtube.svg +3 -0
- package/repo-templates/template-bigworkbag/templates/translations/da.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/de.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/en.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/es.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/fr.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/nb.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/nl.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/sv.njk +26 -0
- package/repo-templates/template-bigworkbag/templates/translations/zh-cn.njk +26 -0
- package/server/extensions/error-logging.js +25 -0
- package/server/index.js +98 -0
- package/server/plugins/hapi-vite.js +61 -0
- package/server/routes/public.js +29 -0
- package/server/routes/views.js +46 -0
- package/server/templates/furniture.njk +67 -0
- package/server/templates/page1.njk +37 -0
- package/server/view-manager.js +59 -0
- package/shared/Dockerfile.brandingRepoPublish +28 -0
- package/shared/README.md +4 -0
- package/vite.config.js +14 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { rimraf } = require('rimraf');
|
|
3
|
+
const { resolveConfig, log } = require('../utils');
|
|
4
|
+
const bundleEntry = require('./build-tasks/bundle-entry');
|
|
5
|
+
const buildCssFromTokens = require('./build-tasks/build-tokens');
|
|
6
|
+
const buildExternalAssets = require('./build-tasks/build-external-assets');
|
|
7
|
+
|
|
8
|
+
module.exports = async (root, options = {}) => {
|
|
9
|
+
const fertConfig = await resolveConfig(root, options);
|
|
10
|
+
|
|
11
|
+
if (!options.only) {
|
|
12
|
+
await rimraf(path.resolve(fertConfig.workingDir, 'dist'));
|
|
13
|
+
await exports.buildTokens(fertConfig, options);
|
|
14
|
+
await exports.bundleEntry(fertConfig, options);
|
|
15
|
+
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
switch (options.only) {
|
|
20
|
+
case 'tokens':
|
|
21
|
+
await exports.buildTokens(fertConfig, options);
|
|
22
|
+
break;
|
|
23
|
+
case 'assets':
|
|
24
|
+
await exports.bundleEntry(fertConfig, options);
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
log.error('"--only param" not recognixed. Not building.');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports = module.exports; // allow default export & { named } exports
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line no-unused-vars
|
|
34
|
+
exports.buildTokens = async (fertConfig, options = {}) => {
|
|
35
|
+
const { workingDir } = fertConfig;
|
|
36
|
+
const tokenDist = path.join(workingDir, `/public/tokens/`);
|
|
37
|
+
|
|
38
|
+
buildCssFromTokens(workingDir, tokenDist);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
exports.bundleEntry = bundleEntry;
|
|
42
|
+
exports.buildExternalAssets = buildExternalAssets;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const open = require('open');
|
|
4
|
+
const chokidar = require('chokidar');
|
|
5
|
+
const { resolveConfig, log } = require('../utils/index.js');
|
|
6
|
+
const { devServer } = require('../../server');
|
|
7
|
+
const { buildTokens, buildExternalAssets } = require('./build.js');
|
|
8
|
+
const {
|
|
9
|
+
BRAND_JSON_FILENAME,
|
|
10
|
+
FERT_CONFIG_FILENAME,
|
|
11
|
+
} = require('../../constants.js');
|
|
12
|
+
|
|
13
|
+
module.exports = async (root, options = {}) => {
|
|
14
|
+
let fertConfig = await resolveConfig(root, options);
|
|
15
|
+
|
|
16
|
+
console.log(
|
|
17
|
+
`Starting branding server for ${chalk.green(
|
|
18
|
+
fertConfig.client.brandName
|
|
19
|
+
)} ${chalk.dim(`(${fertConfig.clientPropertyId})`)}\n`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// watch brand.json & fert.config.js - refresh on change
|
|
23
|
+
const brandPath = path.resolve(fertConfig.workingDir, BRAND_JSON_FILENAME);
|
|
24
|
+
chokidar.watch(brandPath).on('all', async () => {
|
|
25
|
+
await buildTokens(fertConfig);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const configPath = path.resolve(fertConfig.workingDir, FERT_CONFIG_FILENAME);
|
|
29
|
+
chokidar.watch(configPath).on('change', async () => {
|
|
30
|
+
console.log('Config changed, reloading…');
|
|
31
|
+
fertConfig = await resolveConfig(root, options);
|
|
32
|
+
await buildExternalAssets(fertConfig);
|
|
33
|
+
|
|
34
|
+
// update server's config & tell Vite to refresh the page
|
|
35
|
+
server.fertConfig({ update: fertConfig });
|
|
36
|
+
server.app.viteServer.ws.send({
|
|
37
|
+
type: 'full-reload',
|
|
38
|
+
path: '*',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// start server
|
|
43
|
+
const server = await devServer({ start: true, fertConfig });
|
|
44
|
+
|
|
45
|
+
log.info(`\nDev server: ${chalk.cyan.bold(server.info.uri)}`);
|
|
46
|
+
|
|
47
|
+
if (options.open) {
|
|
48
|
+
await open(server.info.uri);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const prompts = require('prompts');
|
|
5
|
+
const uuidValidator = require('uuid-validate');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { cpidLookup } = require('../utils/cpid-lookup');
|
|
8
|
+
|
|
9
|
+
const { isEmptyDir, formatTargetDir, emptyDir } = require('../utils/index');
|
|
10
|
+
const { TEMPLATES } = require('../../constants');
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const templateList = TEMPLATES.map((i) => i.name);
|
|
14
|
+
|
|
15
|
+
module.exports = async (root, options = {}) => {
|
|
16
|
+
const defaultProjectName = 'madgex-{cpid}';
|
|
17
|
+
const argTargetDir = root ? formatTargetDir(root) : null;
|
|
18
|
+
const argTemplate = options.template;
|
|
19
|
+
const argClientPropertyId =
|
|
20
|
+
options.cpid && uuidValidator(options.cpid) ? options.cpid : false;
|
|
21
|
+
|
|
22
|
+
let targetDir = argTargetDir || path.join(cwd, defaultProjectName);
|
|
23
|
+
let result;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
result = await prompts(
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
type: argClientPropertyId ? null : 'text',
|
|
30
|
+
name: 'cpid',
|
|
31
|
+
message: 'clientPropertyId:',
|
|
32
|
+
validate: (cpid) =>
|
|
33
|
+
uuidValidator(cpid)
|
|
34
|
+
? true
|
|
35
|
+
: `Invalid UUID, ensure the correct clientPropertyId is entered`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: argTargetDir ? null : 'text',
|
|
39
|
+
name: 'projectName',
|
|
40
|
+
message: 'Project name:',
|
|
41
|
+
initial: (prev) =>
|
|
42
|
+
defaultProjectName.replace('{cpid}', prev || argClientPropertyId),
|
|
43
|
+
onState: (state) => {
|
|
44
|
+
targetDir = formatTargetDir(state.value) || defaultProjectName;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type:
|
|
49
|
+
argTemplate && templateList.includes(argTemplate) ? null : 'select',
|
|
50
|
+
name: 'template',
|
|
51
|
+
message:
|
|
52
|
+
typeof argTemplate === 'string' &&
|
|
53
|
+
!templateList.includes(argTemplate)
|
|
54
|
+
? `"${argTemplate}" isn't a valid template. Please choose from below: `
|
|
55
|
+
: 'Select a template:',
|
|
56
|
+
initial: 0,
|
|
57
|
+
choices: TEMPLATES.map((i) => {
|
|
58
|
+
return {
|
|
59
|
+
title: i.display,
|
|
60
|
+
value: i.name,
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: () =>
|
|
66
|
+
!fs.existsSync(targetDir) || isEmptyDir(targetDir)
|
|
67
|
+
? null
|
|
68
|
+
: 'confirm',
|
|
69
|
+
name: 'overwrite',
|
|
70
|
+
message: () =>
|
|
71
|
+
(targetDir === '.'
|
|
72
|
+
? 'Current directory'
|
|
73
|
+
: `Target directory "${targetDir}"`) +
|
|
74
|
+
` is not empty. Remove existing files and continue?`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: (_, { overwrite }) => {
|
|
78
|
+
if (overwrite === false) {
|
|
79
|
+
throw new Error(chalk.red('✖') + ' Operation cancelled');
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
name: 'overwriteChecker',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
{
|
|
87
|
+
onCancel: () => {
|
|
88
|
+
throw new Error(chalk.red('✖') + ' Operation cancelled');
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
} catch (cancelled) {
|
|
93
|
+
console.log(cancelled.message);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { overwrite, template, cpid } = result;
|
|
98
|
+
const templateName = template || argTemplate;
|
|
99
|
+
const clientPropertyId = cpid || argClientPropertyId;
|
|
100
|
+
const outDir = targetDir;
|
|
101
|
+
|
|
102
|
+
const spinner = ora(
|
|
103
|
+
`Looking up clientPropertyId: ${chalk.yellow(clientPropertyId)}`
|
|
104
|
+
).start();
|
|
105
|
+
let clientProperties;
|
|
106
|
+
try {
|
|
107
|
+
clientProperties = await cpidLookup(clientPropertyId);
|
|
108
|
+
spinner.succeed(
|
|
109
|
+
`Looking up clientPropertyId: ${chalk.green(clientPropertyId)}`
|
|
110
|
+
);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
spinner.fail(
|
|
113
|
+
`Unable to find client details for '${chalk.red(clientPropertyId)}':`,
|
|
114
|
+
err.message
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (overwrite) {
|
|
121
|
+
emptyDir(outDir);
|
|
122
|
+
} else if (!fs.existsSync(outDir)) {
|
|
123
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\nScaffolding project in ${chalk.bold(outDir)}…`);
|
|
127
|
+
|
|
128
|
+
const templateGlobalsDir = path.resolve(
|
|
129
|
+
__dirname,
|
|
130
|
+
'../../repo-templates',
|
|
131
|
+
`globals`
|
|
132
|
+
);
|
|
133
|
+
const templateDir = path.resolve(
|
|
134
|
+
__dirname,
|
|
135
|
+
'../../repo-templates',
|
|
136
|
+
`template-${templateName}`
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const renameFiles = {
|
|
140
|
+
_gitignore: '.gitignore',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function copy(src, dest) {
|
|
144
|
+
const stat = fs.statSync(src);
|
|
145
|
+
if (stat.isDirectory()) {
|
|
146
|
+
copyDir(src, dest);
|
|
147
|
+
} else {
|
|
148
|
+
fs.copyFileSync(src, dest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function copyDir(srcDir, destDir) {
|
|
153
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
154
|
+
for (const file of fs.readdirSync(srcDir)) {
|
|
155
|
+
const srcFile = path.resolve(srcDir, file);
|
|
156
|
+
const destFile = path.resolve(destDir, file);
|
|
157
|
+
copy(srcFile, destFile);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const write = (file, content) => {
|
|
162
|
+
const targetPath = path.join(outDir, renameFiles[file] ?? file);
|
|
163
|
+
if (content) {
|
|
164
|
+
fs.writeFileSync(targetPath, content);
|
|
165
|
+
} else {
|
|
166
|
+
copy(path.join(templateDir, file), targetPath);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
copy(templateGlobalsDir, outDir);
|
|
171
|
+
|
|
172
|
+
const files = fs.readdirSync(templateDir);
|
|
173
|
+
for (const file of files.filter((f) => f !== 'package.json')) {
|
|
174
|
+
write(file);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// create package.json
|
|
178
|
+
const pkg = JSON.parse(
|
|
179
|
+
fs.readFileSync(path.join(templateDir, 'package.json'), 'utf-8')
|
|
180
|
+
);
|
|
181
|
+
pkg.name = `madgex-${clientPropertyId}`;
|
|
182
|
+
pkg.description = `Branding repo for ${clientProperties.brandName}`;
|
|
183
|
+
write('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
184
|
+
|
|
185
|
+
// create fert.config.js
|
|
186
|
+
const fertConfigJs = fs.readFileSync(
|
|
187
|
+
path.join(outDir, 'fert.config.js'),
|
|
188
|
+
'utf-8'
|
|
189
|
+
);
|
|
190
|
+
write('fert.config.js', fertConfigJs.replace(/{CPID}/gi, clientPropertyId));
|
|
191
|
+
|
|
192
|
+
const cdProjectName = path.relative(cwd, outDir);
|
|
193
|
+
|
|
194
|
+
console.log(`\nDone. Now run:\n`);
|
|
195
|
+
|
|
196
|
+
if (cdProjectName && root !== cwd) {
|
|
197
|
+
console.log(
|
|
198
|
+
` cd ${
|
|
199
|
+
cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
|
|
200
|
+
}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(` npm install`);
|
|
205
|
+
console.log(` npm run dev`);
|
|
206
|
+
console.log();
|
|
207
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Hoek = require('@hapi/hoek');
|
|
4
|
+
const { log } = require('../../utils');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const FormData = require('form-data');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { ASSET_STORE_USER_GUID } = require('../../../constants');
|
|
10
|
+
|
|
11
|
+
module.exports = class AssetStoreUploader {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
Hoek.assert(options.apiUrl, 'apiUrl required');
|
|
14
|
+
Hoek.assert(options.apiKey, 'apiKey required');
|
|
15
|
+
|
|
16
|
+
this.apiUrl = options.apiUrl;
|
|
17
|
+
this.apiKey = options.apiKey;
|
|
18
|
+
this.basePath = options.basePath ?? '/api/assets';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
buildAbsolutePath(relativePath, ...parts) {
|
|
22
|
+
return new URL(
|
|
23
|
+
path.posix.join(this.basePath, relativePath, ...parts),
|
|
24
|
+
this.apiUrl
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async upload(file, dest) {
|
|
29
|
+
const absoluteUrl = this.buildAbsolutePath(dest, path.basename(file));
|
|
30
|
+
const label = absoluteUrl.pathname;
|
|
31
|
+
|
|
32
|
+
log.debug('upload', { file, dest, absoluteUrl });
|
|
33
|
+
|
|
34
|
+
const form = new FormData();
|
|
35
|
+
form.append('data', fs.createReadStream(file));
|
|
36
|
+
form.append('modifiedById', ASSET_STORE_USER_GUID);
|
|
37
|
+
|
|
38
|
+
const spinner = ora(`${chalk.yellow('Uploading')}: ${label}`).start();
|
|
39
|
+
|
|
40
|
+
return axios
|
|
41
|
+
.put(absoluteUrl.href, form, {
|
|
42
|
+
headers: {
|
|
43
|
+
apikey: this.apiKey,
|
|
44
|
+
...form.getHeaders(),
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
.then(() => {
|
|
48
|
+
spinner.succeed(`${label}`);
|
|
49
|
+
})
|
|
50
|
+
.catch((err) => {
|
|
51
|
+
spinner.fail(`Failed to upload '${label}':`, err.message);
|
|
52
|
+
|
|
53
|
+
throw err;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async uploadDir(dir, dest = false) {
|
|
58
|
+
Hoek.assert(dir, 'dir missing');
|
|
59
|
+
|
|
60
|
+
const start = process.hrtime();
|
|
61
|
+
const files = this._getFilesFromDir(dir);
|
|
62
|
+
|
|
63
|
+
// Send files consecutively to avoid straining the api
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const relativeName = path.posix.relative(dir, file);
|
|
66
|
+
const remotePath = path.posix.dirname(dest || relativeName);
|
|
67
|
+
|
|
68
|
+
await this.upload(file, remotePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const end = process.hrtime(start);
|
|
72
|
+
const duration = end[0] * 1e3 + end[1] * 1e-6;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
files,
|
|
76
|
+
duration,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Returns array of all files in provided directory (recursive)
|
|
81
|
+
_getFilesFromDir = (dir, files = []) => {
|
|
82
|
+
fs.readdirSync(dir).forEach((entry) => {
|
|
83
|
+
const name = path.join(dir, entry);
|
|
84
|
+
|
|
85
|
+
if (fs.statSync(name).isDirectory()) {
|
|
86
|
+
this._getFilesFromDir(name, files);
|
|
87
|
+
} else {
|
|
88
|
+
files.push(name);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return files;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
|
|
2
|
+
const { fromEnv } = require('@aws-sdk/credential-providers');
|
|
3
|
+
const { AWS_REGION } = require('../../../constants');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets AWS parameter from the parameter store
|
|
7
|
+
*
|
|
8
|
+
* @function getAwsParam
|
|
9
|
+
*
|
|
10
|
+
* @async
|
|
11
|
+
*
|
|
12
|
+
* @param {string} paramName the name of the parameter we want from AWS parameter store.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<{ value: string | undefined }>} the value of the requested AWS parameter
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
const getAwsParam = async (paramName) => {
|
|
18
|
+
const client = new SSMClient({
|
|
19
|
+
region: AWS_REGION,
|
|
20
|
+
credentials: fromEnv(),
|
|
21
|
+
});
|
|
22
|
+
const input = {
|
|
23
|
+
Name: paramName,
|
|
24
|
+
WithDecryption: true,
|
|
25
|
+
};
|
|
26
|
+
const command = new GetParameterCommand(input);
|
|
27
|
+
let result;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const response = await client.send(command);
|
|
31
|
+
|
|
32
|
+
result = response.Parameter.Value;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.log(`parameter-store error (${paramName}) ${error.message}`, error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = getAwsParam;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const Hoek = require('@hapi/hoek');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { resolveConfig, log } = require('../utils');
|
|
5
|
+
const getAwsParam = require('./publish-tasks/get-aws-parameter');
|
|
6
|
+
const AssetStoreUploader = require('./publish-tasks/asset-store-uploader');
|
|
7
|
+
const {
|
|
8
|
+
ASSET_STORE_API,
|
|
9
|
+
AWS_PARAM_NAME,
|
|
10
|
+
REMOTE_UPLOAD_BASE,
|
|
11
|
+
UPLOAD_DIR,
|
|
12
|
+
} = require('../../constants');
|
|
13
|
+
|
|
14
|
+
module.exports = async (root, options) => {
|
|
15
|
+
const fertConfig = await resolveConfig(root, options);
|
|
16
|
+
const validTargets = ['dev', 'prod'];
|
|
17
|
+
|
|
18
|
+
if (!validTargets.includes(options.target)) {
|
|
19
|
+
throw Error(
|
|
20
|
+
`Missing or invalid --target option. Choose from [${validTargets}]`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const templateCtx = {
|
|
25
|
+
env: options.target === 'prod' ? 'job' : 'jb.dev',
|
|
26
|
+
Environement_Name: options.target === 'prod' ? 'production' : 'dev',
|
|
27
|
+
fertConfig,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const localDir = path.resolve(fertConfig.workingDir, UPLOAD_DIR);
|
|
31
|
+
const apiUrl = Hoek.reachTemplate(templateCtx, ASSET_STORE_API);
|
|
32
|
+
const remoteBasePath = Hoek.reachTemplate(templateCtx, REMOTE_UPLOAD_BASE);
|
|
33
|
+
const apiKeyParamPath = Hoek.reachTemplate(templateCtx, AWS_PARAM_NAME);
|
|
34
|
+
|
|
35
|
+
const apiKey = await getAwsParam(apiKeyParamPath);
|
|
36
|
+
|
|
37
|
+
log.info(
|
|
38
|
+
`Publishing ${chalk.cyan(localDir)} to ${chalk.green.bold(options.target)}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// send to S3 using the Asset Store API
|
|
42
|
+
const assetStore = new AssetStoreUploader({
|
|
43
|
+
apiUrl,
|
|
44
|
+
apiKey,
|
|
45
|
+
basePath: remoteBasePath,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await assetStore.uploadDir(localDir);
|
|
49
|
+
|
|
50
|
+
log.success(`Publish complete in ${(result.duration / 1000).toFixed(0)}s\n`);
|
|
51
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Fetch provides support for node 16 and lower
|
|
2
|
+
const fetch = require('node-fetch-commonjs');
|
|
3
|
+
const storage = require('node-persist');
|
|
4
|
+
const dayjs = require('dayjs');
|
|
5
|
+
const duration = require('dayjs/plugin/duration');
|
|
6
|
+
const { PROPERTY_ID_API, TMP_DIR } = require('../../constants');
|
|
7
|
+
|
|
8
|
+
dayjs.extend(duration);
|
|
9
|
+
|
|
10
|
+
exports.doCpidLookup = async (clientPropertyId) => {
|
|
11
|
+
const API_URL = new URL(clientPropertyId, PROPERTY_ID_API).toString();
|
|
12
|
+
|
|
13
|
+
const res = await fetch(API_URL);
|
|
14
|
+
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw Error(`Unable to lookup CPID [${clientPropertyId}]`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { data: results } = await res.json();
|
|
20
|
+
const { brandName, parentId: parentCpid } = results;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
brandName,
|
|
24
|
+
parentCpid: parentCpid ?? false,
|
|
25
|
+
rootClientPropertyId: parentCpid ?? clientPropertyId,
|
|
26
|
+
isAffiliate: !!parentCpid,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.cpidLookup = async (clientPropertyId) => {
|
|
31
|
+
await storage.init({
|
|
32
|
+
dir: `${TMP_DIR}/fert-cpid-cache`,
|
|
33
|
+
ttl: dayjs.duration(1, 'week').asMilliseconds(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const value = await storage.getItem(clientPropertyId);
|
|
37
|
+
|
|
38
|
+
if (value) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await this.doCpidLookup(clientPropertyId);
|
|
43
|
+
|
|
44
|
+
await storage.updateItem(clientPropertyId, result);
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const { VERSION } = require('../../constants');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const debug = require('debug');
|
|
7
|
+
const debugLog = debug('fert');
|
|
8
|
+
const uuidValidator = require('uuid-validate');
|
|
9
|
+
const Hoek = require('@hapi/hoek');
|
|
10
|
+
const { cpidLookup } = require('./cpid-lookup');
|
|
11
|
+
const resolveExternalAssets = require('./resolve-external-assets');
|
|
12
|
+
|
|
13
|
+
exports.printBanner = () => {
|
|
14
|
+
console.log(chalk`\n{green.bold Fert} v${VERSION}\n`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
exports.log = {
|
|
18
|
+
info: (...args) => {
|
|
19
|
+
console.log(...args);
|
|
20
|
+
},
|
|
21
|
+
success: (...args) => {
|
|
22
|
+
console.log(chalk.green.bold('✔'), ...args);
|
|
23
|
+
},
|
|
24
|
+
error: (...args) => {
|
|
25
|
+
console.log(chalk.red.bold('✖'), ...args);
|
|
26
|
+
},
|
|
27
|
+
debug: (...args) => {
|
|
28
|
+
debugLog(...args);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
exports.resolveConfig = async (root, options = {}) => {
|
|
33
|
+
const defaults = {
|
|
34
|
+
clientPropertyId: null,
|
|
35
|
+
entry: 'src/index.js',
|
|
36
|
+
externalAssets: {
|
|
37
|
+
links: [],
|
|
38
|
+
scripts: [],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const workingDir = root ? path.resolve(root) : path.resolve(process.cwd());
|
|
43
|
+
const fertConfig = Hoek.applyToDefaults(
|
|
44
|
+
defaults,
|
|
45
|
+
this.loadConfigFromFile(workingDir)
|
|
46
|
+
);
|
|
47
|
+
const client = await cpidLookup(fertConfig.clientPropertyId);
|
|
48
|
+
|
|
49
|
+
Hoek.assert(
|
|
50
|
+
uuidValidator(fertConfig.clientPropertyId),
|
|
51
|
+
`Invalid clientPropertyId specified in fert.config.js: ${fertConfig.clientPropertyId}`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
fertConfig.externalAssets = resolveExternalAssets(fertConfig.externalAssets);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...fertConfig,
|
|
58
|
+
workingDir,
|
|
59
|
+
cli: {
|
|
60
|
+
...options,
|
|
61
|
+
},
|
|
62
|
+
client,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
exports.loadConfigFromFile = (workingDir) => {
|
|
67
|
+
const configPath = path.resolve(workingDir, 'fert.config.js');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// always return fresh config
|
|
71
|
+
if (require.cache[configPath]) {
|
|
72
|
+
delete require.cache[configPath];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fertConfig = require(configPath);
|
|
76
|
+
|
|
77
|
+
return fertConfig;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.log.error(
|
|
80
|
+
`Failed to load fert.config.js - ensure you're running fert on a branding repo & config exists.\n`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
exports.ensureTrailingSlash = (str) => {
|
|
88
|
+
const lastChar = str.at(-1);
|
|
89
|
+
|
|
90
|
+
return lastChar === '/' ? str : `${str}/`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
exports.exists = async (path) => {
|
|
94
|
+
try {
|
|
95
|
+
await fsp.access(path);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
exports.isEmptyDir = function isEmptyDir(path) {
|
|
103
|
+
const files = fs.readdirSync(path);
|
|
104
|
+
return files.length === 0 || (files.length === 1 && files[0] === '.git');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
exports.formatTargetDir = function formatTargetDir(targetDir) {
|
|
108
|
+
return targetDir?.trim().replace(/\/+$/g, '');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
exports.emptyDir = function emptyDir(dir) {
|
|
112
|
+
if (!fs.existsSync(dir)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const file of fs.readdirSync(dir)) {
|
|
116
|
+
if (file === '.git') {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
};
|