Skip to content

Commit f592db5

Browse files
committed
feat: strf-9258 Stencil Pull: Provide an activate option
1 parent dae631a commit f592db5

File tree

8 files changed

+361
-116
lines changed

8 files changed

+361
-116
lines changed

bin/stencil-pull.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ require('colors');
44

55
const { PACKAGE_INFO, API_HOST } = require('../constants');
66
const program = require('../lib/commander');
7-
const stencilPull = require('../lib/stencil-pull');
7+
const StencilPull = require('../lib/stencil-pull');
88
const { checkNodeVersion } = require('../lib/cliCommon');
99
const { printCliResultErrorAndExit } = require('../lib/cliCommon');
1010

@@ -22,6 +22,7 @@ program
2222
'specify the channel ID of the storefront to pull configuration from',
2323
parseInt,
2424
)
25+
.option('-a, --activate [variationname]', 'specify the variation of the theme to activate')
2526
.parse(process.argv);
2627

2728
checkNodeVersion();
@@ -32,11 +33,8 @@ const options = {
3233
saveConfigName: cliOptions.filename,
3334
channelId: cliOptions.channel_id,
3435
saved: cliOptions.saved || false,
35-
applyTheme: true, // fix to be compatible with stencil-push.utils
36+
applyTheme: true,
37+
activate: cliOptions.activate,
3638
};
3739

38-
stencilPull(options, (err) => {
39-
if (err) {
40-
printCliResultErrorAndExit(err);
41-
}
42-
});
40+
new StencilPull().run(options).catch(printCliResultErrorAndExit);

bin/stencil-start.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ program
1010
.version(PACKAGE_INFO.version)
1111
.option('-o, --open', 'Automatically open default browser')
1212
.option('-v, --variation [name]', 'Set which theme variation to use while developing')
13-
.option('-c, --channelId [channelId]', 'Set the channel id for the storefront')
13+
.option('-c, --channelId [channelId]', 'Set the channel id for the storefront', parseInt)
1414
.option('--host [hostname]', 'specify the api host')
1515
.option(
1616
'--tunnel [name]',

lib/stencil-pull.js

Lines changed: 194 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,196 @@
1-
const async = require('async');
2-
const stencilPushUtils = require('./stencil-push.utils');
3-
const stencilPullUtils = require('./stencil-pull.utils');
4-
5-
function stencilPull(options = {}, callback) {
6-
async.waterfall(
7-
[
8-
async.constant(options),
9-
stencilPushUtils.readStencilConfigFile,
10-
stencilPushUtils.getStoreHash,
11-
stencilPushUtils.getChannels,
12-
stencilPushUtils.promptUserForChannel,
13-
stencilPullUtils.getChannelActiveTheme,
14-
stencilPullUtils.getThemeConfiguration,
15-
stencilPullUtils.mergeThemeConfiguration,
16-
],
17-
callback,
18-
);
1+
const fsModule = require('fs');
2+
const StencilConfigManager = require('./StencilConfigManager');
3+
const themeApiClientModule = require('./theme-api-client');
4+
const stencilPushUtilsModule = require('./stencil-push.utils');
5+
const fsUtilsModule = require('./utils/fsUtils');
6+
7+
require('colors');
8+
9+
class StencilPull {
10+
constructor({
11+
stencilConfigManager = new StencilConfigManager(),
12+
themeApiClient = themeApiClientModule,
13+
stencilPushUtils = stencilPushUtilsModule,
14+
fsUtils = fsUtilsModule,
15+
fs = fsModule,
16+
} = {}) {
17+
this._stencilConfigManager = stencilConfigManager;
18+
this._themeApiClient = themeApiClient;
19+
this._stencilPushUtils = stencilPushUtils;
20+
this._fsUtils = fsUtils;
21+
this._fs = fs;
22+
}
23+
24+
/**
25+
* @param {Object} cliOptions
26+
*/
27+
async run(cliOptions) {
28+
const stencilConfig = await this._stencilConfigManager.read();
29+
const storeHash = await this._themeApiClient.getStoreHash({
30+
storeUrl: stencilConfig.normalStoreUrl,
31+
});
32+
33+
let { channelId } = cliOptions;
34+
if (!channelId) {
35+
const channels = await this._themeApiClient.getStoreChannels({
36+
accessToken: stencilConfig.accessToken,
37+
apiHost: cliOptions.apiHost,
38+
storeHash,
39+
});
40+
41+
channelId = await this._stencilPushUtils.promptUserToSelectChannel(channels);
42+
}
43+
44+
const activeTheme = await this.getActiveTheme({
45+
accessToken: stencilConfig.accessToken,
46+
apiHost: cliOptions.apiHost,
47+
storeHash,
48+
channelId,
49+
50+
});
51+
52+
console.log('ok'.green + ` -- Fetched theme details for channel ${channelId}`);
53+
54+
const variations = await this._themeApiClient.getVariationsByThemeId({
55+
accessToken: stencilConfig.accessToken,
56+
apiHost: cliOptions.apiHost,
57+
themeId: activeTheme.active_theme_uuid,
58+
storeHash,
59+
});
60+
61+
const variationId = this._stencilPushUtils.getActivatedVariation(variations, cliOptions.activate);
62+
63+
const remoteThemeConfiguration = await this.getThemeConfiguration({
64+
saved: cliOptions.saved,
65+
activeTheme,
66+
accessToken: stencilConfig.accessToken,
67+
apiHost: cliOptions.apiHost,
68+
storeHash,
69+
variationId,
70+
});
71+
72+
console.log('ok'.green + ` -- Fetched ${cliOptions.saved ? 'saved' : 'active'} configuration`);
73+
74+
await this.mergeThemeConfiguration({
75+
variationId,
76+
activate: cliOptions.activate,
77+
remoteThemeConfiguration,
78+
saveConfigName: cliOptions.saveConfigName
79+
});
80+
81+
return true;
82+
}
83+
84+
/**
85+
* @param {Object} options
86+
* @param {String} options.accessToken
87+
* @param {String} options.apiHost
88+
* @param {String} options.storeHash
89+
* @param {Number} options.channelId
90+
*/
91+
async getActiveTheme({ accessToken, apiHost, storeHash, channelId }) {
92+
const activeTheme = await this._themeApiClient.getChannelActiveTheme({
93+
accessToken,
94+
apiHost,
95+
storeHash,
96+
channelId,
97+
});
98+
99+
return activeTheme;
100+
}
101+
102+
/**
103+
* @param {Object} options
104+
* @param {Boolean} options.saved
105+
* @param {Object} options.activeTheme
106+
* @param {String} options.accessToken
107+
* @param {String} options.apiHost
108+
* @param {String} options.storeHash
109+
* @param {String} options.variationId
110+
*/
111+
async getThemeConfiguration({
112+
saved,
113+
activeTheme,
114+
accessToken,
115+
apiHost,
116+
storeHash,
117+
variationId
118+
}) {
119+
const configurationId = saved
120+
? activeTheme.saved_theme_configuration_uuid
121+
: activeTheme.active_theme_configuration_uuid;
122+
123+
const remoteThemeConfiguration = await this._themeApiClient.getThemeConfiguration({
124+
accessToken,
125+
apiHost,
126+
storeHash,
127+
themeId: activeTheme.active_theme_uuid,
128+
configurationId,
129+
variationId
130+
});
131+
132+
return remoteThemeConfiguration;
133+
}
134+
135+
/**
136+
* @param {Object} options
137+
* @param {String} options.variationId
138+
* @param {String} options.activate
139+
* @param {Object} options.remoteThemeConfiguration
140+
* @param {Object} options.remoteThemeConfiguration
141+
*/
142+
async mergeThemeConfiguration({ variationId, activate, remoteThemeConfiguration, saveConfigName }) {
143+
const localConfig = await this._fsUtils.parseJsonFile('config.json');
144+
let diffDetected = false;
145+
let { settings } = localConfig;
146+
147+
if (variationId) {
148+
({ settings } = localConfig.variations.find(v => v.name === activate));
149+
}
150+
151+
// For any keys the remote configuration has in common with the local configuration,
152+
// overwrite the local configuration if the remote configuration differs
153+
for (const [key, remoteVal] of Object.entries(remoteThemeConfiguration.settings)) {
154+
if (!(key in settings)) {
155+
continue;
156+
}
157+
const localVal = settings[key];
158+
159+
// Check for different types, and throw an error if they are found
160+
if (typeof localVal !== typeof remoteVal) {
161+
throw new Error(
162+
`Theme configuration key "${key}" cannot be merged because it is not of the same type. ` +
163+
`Remote configuration is of type ${typeof remoteVal} while local configuration is of type ${typeof localVal}.`,
164+
);
165+
}
166+
167+
// If a different value is found, overwrite the local config
168+
if (!_.isEqual(localVal, remoteVal)) {
169+
settings[key] = remoteVal;
170+
diffDetected = true;
171+
}
172+
}
173+
174+
// Does a file need to be written?
175+
if (diffDetected || saveConfigName !== 'config.json') {
176+
if (diffDetected) {
177+
console.log('ok'.green + ' -- Remote configuration merged with local configuration');
178+
} else {
179+
console.log(
180+
'ok'.green + ' -- Remote and local configurations are in sync for all common keys',
181+
);
182+
}
183+
184+
await this._fs.promises.writeFile(saveConfigName, JSON.stringify(localConfig, null, 2));
185+
console.log('ok'.green + ` -- Configuration written to ${saveConfigName}`);
186+
} else {
187+
console.log(
188+
'ok'.green +
189+
` -- Remote and local configurations are in sync for all common keys, no action taken`,
190+
);
191+
}
192+
}
19193
}
20194

21-
module.exports = stencilPull;
195+
196+
module.exports = StencilPull;

lib/stencil-pull.spec.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const stencilPushUtilsModule = require('./stencil-push.utils');
2+
const StencilPull = require('./stencil-pull');
3+
4+
afterAll(() => jest.restoreAllMocks());
5+
6+
describe('StencilStart unit tests', () => {
7+
const accessToken = 'accessToken_value';
8+
const normalStoreUrl = 'https://www.example.com';
9+
const channelId = 1;
10+
const storeHash = 'storeHash_value';
11+
const channels = [{
12+
channel_id: channelId,
13+
url: normalStoreUrl,
14+
}];
15+
const activeThemeUuid = 'activeThemeUuid_value';
16+
const variations = [
17+
{uuid: 1, name: "Light"},
18+
{uuid: 2, name: "Bold"},
19+
];
20+
21+
const localThemeConfiguration = {
22+
settings: {
23+
24+
},
25+
variations: [
26+
{
27+
name: "Light",
28+
settings: {}
29+
}
30+
]
31+
};
32+
const remoteThemeConfiguration = {
33+
settings: {
34+
"body-font": "Google_Source+Sans+Pro_400",
35+
"headings-font": "Google_Roboto_400",
36+
"color-textBase": "#ffffff",
37+
"color-textBase--hover": "#bbbbbb",
38+
}
39+
};
40+
41+
const saveConfigName = 'config.json';
42+
const cliOptions = {
43+
channelId,
44+
saveConfigName,
45+
saved: false,
46+
applyTheme: true,
47+
};
48+
const stencilConfig = {
49+
accessToken,
50+
normalStoreUrl,
51+
};
52+
53+
const getThemeApiClientStub = () => ({
54+
getStoreHash: jest.fn().mockResolvedValue(storeHash),
55+
getStoreChannels: jest.fn().mockResolvedValue(channels),
56+
getChannelActiveTheme: jest.fn().mockResolvedValue({
57+
active_theme_uuid: activeThemeUuid,
58+
}),
59+
getVariationsByThemeId: jest.fn().mockResolvedValue(variations),
60+
getThemeConfiguration: jest.fn().mockResolvedValue(remoteThemeConfiguration),
61+
});
62+
const getFsUtilsStub = () => ({
63+
parseJsonFile: jest.fn().mockResolvedValue(localThemeConfiguration)
64+
});
65+
const getFsModuleStub = () => ({
66+
promises: {
67+
writeFile: jest.fn()
68+
}
69+
});
70+
const getStencilConfigManagerStub = () => ({
71+
read: jest.fn().mockResolvedValue(stencilConfig)
72+
});
73+
const getStencilPushUtilsStub = () => ({
74+
promptUserToSelectChannel: jest.fn(),
75+
getActivatedVariation: stencilPushUtilsModule.getActivatedVariation
76+
});
77+
78+
const createStencilPullInstance = ({
79+
stencilConfigManager,
80+
themeApiClient,
81+
stencilPushUtils,
82+
fsUtils,
83+
fsModule
84+
} = {}) => {
85+
const passedArgs = {
86+
stencilConfigManager: stencilConfigManager || getStencilConfigManagerStub(),
87+
themeApiClient: themeApiClient || getThemeApiClientStub(),
88+
stencilPushUtils: stencilPushUtils || getStencilPushUtilsStub(),
89+
fsUtils: fsUtils || getFsUtilsStub(),
90+
fs: fsModule || getFsModuleStub(),
91+
};
92+
const instance = new StencilPull(passedArgs);
93+
94+
return {
95+
passedArgs,
96+
instance,
97+
};
98+
};
99+
100+
describe('constructor', () => {
101+
it('should create an instance of StencilPull without options parameters passed', () => {
102+
const instance = new StencilPull();
103+
104+
expect(instance).toBeInstanceOf(StencilPull);
105+
});
106+
107+
it('should create an instance of StencilStart with all options parameters passed', () => {
108+
const { instance } = createStencilPullInstance();
109+
110+
expect(instance).toBeInstanceOf(StencilPull);
111+
});
112+
});
113+
114+
describe('run', () => {
115+
it('should run stencil pull with channel id', async () => {
116+
const { instance } = createStencilPullInstance();
117+
118+
const result = await instance.run(cliOptions);
119+
120+
expect(result).toBe(true);
121+
});
122+
123+
it('should run stencil pull without channel id', async () => {
124+
const themeApiClient = getThemeApiClientStub();
125+
const { instance } = createStencilPullInstance({ themeApiClient });
126+
127+
instance.run({ saveConfigName });
128+
129+
const result = await instance.run(cliOptions);
130+
131+
expect(themeApiClient.getStoreChannels).toHaveBeenCalled()
132+
expect(result).toBe(true);
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)