From 6f3d2dcaa7205b7c8c54da729c936c649d109e38 Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Thu, 29 Oct 2020 23:09:39 +0200 Subject: [PATCH] feat: (strf-8747) split .stencil file into 2 configs --- lib/StencilConfigManager.js | 107 ++++- lib/StencilConfigManager.spec.js | 406 ++++++++++++++++++ lib/release/release.js | 6 +- lib/stencil-init.js | 6 +- lib/stencil-init.spec.js | 41 +- lib/stencil-push.utils.js | 2 +- lib/stencil-start.js | 32 +- test/_mocks/themes/valid/.stencil | 5 - test/_mocks/themes/valid/config.stencil.json | 10 + test/_mocks/themes/valid/secrets.stencil.json | 3 + 10 files changed, 557 insertions(+), 61 deletions(-) create mode 100644 lib/StencilConfigManager.spec.js delete mode 100644 test/_mocks/themes/valid/.stencil create mode 100644 test/_mocks/themes/valid/config.stencil.json create mode 100644 test/_mocks/themes/valid/secrets.stencil.json diff --git a/lib/StencilConfigManager.js b/lib/StencilConfigManager.js index beaa84d7..7ebaf319 100644 --- a/lib/StencilConfigManager.js +++ b/lib/StencilConfigManager.js @@ -6,23 +6,56 @@ const fsUtilsModule = require('./utils/fsUtils'); const { THEME_PATH } = require('../constants'); class StencilConfigManager { - constructor({ themePath = THEME_PATH, fs = fsModule, fsUtils = fsUtilsModule } = {}) { - this.configFileName = '.stencil'; + constructor({ + themePath = THEME_PATH, + fs = fsModule, + fsUtils = fsUtilsModule, + logger = console, + } = {}) { + this.oldConfigFileName = '.stencil'; + this.configFileName = 'config.stencil.json'; + this.secretsFileName = 'secrets.stencil.json'; this.themePath = themePath; + this.oldConfigPath = path.join(themePath, this.oldConfigFileName); this.configPath = path.join(themePath, this.configFileName); + this.secretsPath = path.join(themePath, this.secretsFileName); + this.secretFieldsSet = new Set(['accessToken', 'githubToken']); this._fs = fs; this._fsUtils = fsUtils; + this._logger = logger; } /** - * @returns {object|null} * @param {boolean} ignoreFileNotExists + * @param {boolean} ignoreMissingFields + * @returns {object|null} */ - async readStencilConfig(ignoreFileNotExists = false) { - if (this._fs.existsSync(this.configPath)) { - return this._fsUtils.parseJsonFile(this.configPath); + async read(ignoreFileNotExists = false, ignoreMissingFields = false) { + if (this._fs.existsSync(this.oldConfigPath)) { + let parsedConfig; + try { + parsedConfig = await this._fsUtils.parseJsonFile(this.oldConfigPath); + // Tolerate broken files. We should migrate the old config first + // and then validation will throw an error about missing fields + // eslint-disable-next-line no-empty + } catch { + parsedConfig = {}; + } + await this._migrateOldConfig(parsedConfig); + return this._validateStencilConfig(parsedConfig, ignoreMissingFields); + } + + const generalConfig = this._fs.existsSync(this.configPath) + ? await this._fsUtils.parseJsonFile(this.configPath) + : null; + const secretsConfig = this._fs.existsSync(this.secretsPath) + ? await this._fsUtils.parseJsonFile(this.secretsPath) + : null; + if (generalConfig || secretsConfig) { + const parsedConfig = { ...generalConfig, ...secretsConfig }; + return this._validateStencilConfig(parsedConfig, ignoreMissingFields); } if (ignoreFileNotExists) { @@ -35,8 +68,66 @@ class StencilConfigManager { /** * @param {object} config */ - saveStencilConfig(config) { - this._fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); + async save(config) { + const { generalConfig, secretsConfig } = this._splitStencilConfig(config); + + await this._fs.promises.writeFile(this.configPath, JSON.stringify(generalConfig, null, 2)); + await this._fs.promises.writeFile(this.secretsPath, JSON.stringify(secretsConfig, null, 2)); + } + + /** + * @private + * @param {object} config + */ + _splitStencilConfig(config) { + return Object.entries(config).reduce( + (res, [key, value]) => { + if (this.secretFieldsSet.has(key)) { + res.secretsConfig[key] = value; + } else { + res.generalConfig[key] = value; + } + return res; + }, + { secretsConfig: {}, generalConfig: {} }, + ); + } + + /** + * @private + * @param {object} config + * @param {boolean} ignoreMissingFields + * @returns {object} + */ + _validateStencilConfig(config, ignoreMissingFields) { + if (!ignoreMissingFields && (!config.normalStoreUrl || !config.customLayouts)) { + throw new Error( + 'Error: Your stencil config is outdated. Please run'.red + + ' $ stencil init'.cyan + + ' again.'.red, + ); + } + return config; + } + + /** + * @private + * @param {object} config + */ + async _migrateOldConfig(config) { + this._logger.log( + `Detected a deprecated ${this.oldConfigFileName.cyan} file.\n` + + `It will be replaced with ${this.configFileName.cyan} and ${this.secretsFileName.cyan}\n`, + ); + + await this.save(config); + await this._fs.promises.unlink(this.oldConfigPath); + + this._logger.log( + `The deprecated ${this.oldConfigFileName.cyan} file was successfully replaced.\n` + + `Make sure to add ${this.secretsFileName.cyan} to .gitignore.\n` + + `${this.configFileName.cyan} can by tracked by git if you wish.\n`, + ); } } diff --git a/lib/StencilConfigManager.spec.js b/lib/StencilConfigManager.spec.js new file mode 100644 index 00000000..e1fe2dd3 --- /dev/null +++ b/lib/StencilConfigManager.spec.js @@ -0,0 +1,406 @@ +const path = require('path'); +const StencilConfigManager = require('./StencilConfigManager'); + +const defaultThemePath = './test/_mocks/themes/valid/'; +const defaultOldConfigPath = path.join(defaultThemePath, '.stencil'); +const defaultConfigPath = path.join(defaultThemePath, 'config.stencil.json'); +const defaultSecretsPath = path.join(defaultThemePath, 'secrets.stencil.json'); +const getGeneralConfig = () => ({ + customLayouts: { + brand: { + a: 'aaaa', + }, + category: {}, + page: { + b: 'bbbb', + }, + product: {}, + }, + normalStoreUrl: 'https://url-from-stencilConfig.mybigcommerce.com', + port: 3001, +}); +const getSecretsConfig = () => ({ + accessToken: 'accessToken_from_stencilConfig', + githubToken: 'githubToken_1234567890', +}); +const getStencilConfig = () => ({ + ...getGeneralConfig(), + ...getSecretsConfig(), +}); +const getFsStub = () => ({ + promises: { + unlink: jest.fn(), + writeFile: jest.fn(), + }, + existsSync: jest.fn(), +}); +const getFsUtilsStub = () => ({ + parseJsonFile: jest.fn().mockImplementation((filePath) => { + if (filePath === defaultConfigPath) return getGeneralConfig(); + if (filePath === defaultSecretsPath) return getSecretsConfig(); + return getStencilConfig(); + }), +}); +const getLoggerStub = () => ({ + log: jest.fn(), + error: jest.fn(), +}); + +const createStencilConfigManagerInstance = ({ themePath, fs, fsUtils, logger } = {}) => { + const passedArgs = { + themePath: themePath || defaultThemePath, + fs: fs || getFsStub(), + fsUtils: fsUtils || getFsUtilsStub(), + logger: logger || getLoggerStub(), + }; + const instance = new StencilConfigManager(passedArgs); + + return { + passedArgs, + instance, + }; +}; + +afterEach(() => jest.resetAllMocks()); + +describe('StencilConfigManager unit tests', () => { + describe('constructor', () => { + it('should create an instance of StencilConfigManager without options parameters passed', async () => { + const instance = new StencilConfigManager(); + + expect(instance).toBeInstanceOf(StencilConfigManager); + }); + + it('should create an instance of StencilConfigManager with all options parameters passed', async () => { + const { instance } = createStencilConfigManagerInstance(); + + expect(instance).toBeInstanceOf(StencilConfigManager); + }); + }); + + describe('read', () => { + describe('when no config files exit', () => { + it('should return null if ignoreFileNotExists == true', async () => { + const fsStub = getFsStub(); + fsStub.existsSync.mockReturnValue(false); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + }); + const res = await instance.read(true); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + expect(res).toBeNull(); + }); + + it('should throw an error if ignoreFileNotExists == false', async () => { + const fsStub = getFsStub(); + fsStub.existsSync.mockReturnValue(false); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + }); + + await expect(() => instance.read(false)).rejects.toThrow( + 'Please run'.red + ' $ stencil init'.cyan + ' first.'.red, + ); + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + }); + }); + + describe('when an old config file exists', () => { + it('should replace an old config file with new ones and return the parsed config if it is valid', async () => { + const loggerStub = getLoggerStub(); + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath === defaultOldConfigPath, + ); + + const { instance } = createStencilConfigManagerInstance({ + logger: loggerStub, + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const saveStencilConfigSpy = jest.spyOn(instance, 'save'); + const res = await instance.read(); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(1); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(1); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(loggerStub.log).toHaveBeenCalledTimes(2); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`will be replaced`), + ); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`was successfully replaced`), + ); + + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledWith(getStencilConfig()); + + expect(res).toEqual(getStencilConfig()); + }); + + it('should replace an old config file with new ones and throw an error then if the parsed config is broken', async () => { + const loggerStub = getLoggerStub(); + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath === defaultOldConfigPath, + ); + fsUtilsStub.parseJsonFile.mockRejectedValue(new Error('kinda broken json file')); + + const { instance } = createStencilConfigManagerInstance({ + logger: loggerStub, + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const saveStencilConfigSpy = jest.spyOn(instance, 'save'); + + await expect(() => instance.read()).rejects.toThrow( + // Should ignore the error above about a broken file and throw an error about missing fields + 'Your stencil config is outdated', + ); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(1); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(1); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(loggerStub.log).toHaveBeenCalledTimes(2); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`will be replaced`), + ); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`was successfully replaced`), + ); + + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledWith({}); + }); + + describe('when the parsed config has missing required fields', () => { + const getConfigWithMissingFields = () => ({ + port: 12345, + }); + + it('should replace an old config file with new ones and throw an error then if ignoreMissingFields=false', async () => { + const stencilConfig = getConfigWithMissingFields(); + const loggerStub = getLoggerStub(); + const fsStub = getFsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath === defaultOldConfigPath, + ); + const fsUtilsStub = getFsUtilsStub(); + fsUtilsStub.parseJsonFile.mockResolvedValue(stencilConfig); + + const { instance } = createStencilConfigManagerInstance({ + logger: loggerStub, + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const saveStencilConfigSpy = jest.spyOn(instance, 'save'); + + await expect(() => instance.read(false, false)).rejects.toThrow( + 'Your stencil config is outdated', + ); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(1); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(1); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(loggerStub.log).toHaveBeenCalledTimes(2); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`will be replaced`), + ); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`was successfully replaced`), + ); + + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledWith(stencilConfig); + }); + + it('should replace an old config file with new ones and return parsed config if ignoreMissingFields=true', async () => { + const stencilConfig = getConfigWithMissingFields(); + const loggerStub = getLoggerStub(); + const fsStub = getFsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath === defaultOldConfigPath, + ); + const fsUtilsStub = getFsUtilsStub(); + fsUtilsStub.parseJsonFile.mockResolvedValue(stencilConfig); + + const { instance } = createStencilConfigManagerInstance({ + logger: loggerStub, + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const saveStencilConfigSpy = jest.spyOn(instance, 'save'); + const res = await instance.read(false, true); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(1); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(1); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultOldConfigPath); + + expect(loggerStub.log).toHaveBeenCalledTimes(2); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`will be replaced`), + ); + expect(loggerStub.log).toHaveBeenCalledWith( + expect.stringMatching(`was successfully replaced`), + ); + + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledWith(stencilConfig); + + expect(res).toEqual(stencilConfig); + }); + }); + }); + + describe('whe the new config files exist and an old config file do not exist', () => { + it('should read the config files and return the parsed result if both secrets and general config files exist and valid', async () => { + const generalConfig = getGeneralConfig(); + const secretsConfig = getSecretsConfig(); + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath !== defaultOldConfigPath, + ); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const res = await instance.read(); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultSecretsPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(2); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultConfigPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultSecretsPath); + + expect(res).toEqual({ ...generalConfig, ...secretsConfig }); + }); + + it("should read general config if it exists and skip secrets config if it doesn't exist and return the parsed result", async () => { + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation((filePath) => filePath === defaultConfigPath); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + fsUtils: fsUtilsStub, + }); + const res = await instance.read(); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultSecretsPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(1); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultConfigPath); + + expect(res).toEqual(getGeneralConfig()); + }); + + it('should throw an error if the parsed config does not contain normalStoreUrl', async () => { + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath !== defaultOldConfigPath, + ); + fsUtilsStub.parseJsonFile.mockImplementation((filePath) => { + const generalConfig = getGeneralConfig(); + delete generalConfig.normalStoreUrl; + + if (filePath === defaultConfigPath) return generalConfig; + return getSecretsConfig(); + }); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + fsUtils: fsUtilsStub, + }); + await expect(() => instance.read()).rejects.toThrow( + 'Your stencil config is outdated', + ); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultSecretsPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(2); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultConfigPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultSecretsPath); + }); + + it('should throw an error if the parsed config does not contain customLayouts', async () => { + const fsStub = getFsStub(); + const fsUtilsStub = getFsUtilsStub(); + fsStub.existsSync.mockImplementation( + (filePath) => filePath !== defaultOldConfigPath, + ); + fsUtilsStub.parseJsonFile.mockImplementation((filePath) => { + const generalConfig = getGeneralConfig(); + delete generalConfig.customLayouts; + + if (filePath === defaultConfigPath) return generalConfig; + return getSecretsConfig(); + }); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + fsUtils: fsUtilsStub, + }); + await expect(() => instance.read()).rejects.toThrow( + 'Your stencil config is outdated', + ); + + expect(fsStub.existsSync).toHaveBeenCalledTimes(3); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultOldConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultConfigPath); + expect(fsStub.existsSync).toHaveBeenCalledWith(defaultSecretsPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledTimes(2); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultConfigPath); + expect(fsUtilsStub.parseJsonFile).toHaveBeenCalledWith(defaultSecretsPath); + }); + }); + }); + + describe('save', () => { + it('should call fs.writeFile with the serialized configs for secrets and general config fields', async () => { + const stencilConfig = getStencilConfig(); + const serializedGeneralConfig = JSON.stringify(getGeneralConfig(), null, 2); + const serializedSecretsConfig = JSON.stringify(getSecretsConfig(), null, 2); + const fsStub = getFsStub(); + + const { instance } = createStencilConfigManagerInstance({ + fs: fsStub, + }); + await instance.save(stencilConfig); + + expect(fsStub.promises.writeFile).toHaveBeenCalledTimes(2); + expect(fsStub.promises.writeFile).toHaveBeenCalledWith( + defaultConfigPath, + serializedGeneralConfig, + ); + expect(fsStub.promises.writeFile).toHaveBeenCalledWith( + defaultSecretsPath, + serializedSecretsConfig, + ); + }); + }); +}); diff --git a/lib/release/release.js b/lib/release/release.js index 72f9849f..9840456e 100644 --- a/lib/release/release.js +++ b/lib/release/release.js @@ -17,15 +17,15 @@ const themeConfigManager = ThemeConfig.getInstance(THEME_PATH); const stencilConfigManager = new StencilConfigManager(); async function saveGithubToken(githubToken) { - const data = (await stencilConfigManager.readStencilConfig(true)) || {}; + const data = (await stencilConfigManager.read(true)) || {}; data.githubToken = githubToken; - await this.stencilConfigManager.saveStencilConfig(data); + await this.stencilConfigManager.save(data); } async function getGithubToken() { - const data = (await stencilConfigManager.readStencilConfig(true)) || {}; + const data = (await stencilConfigManager.read(true)) || {}; return data.githubToken; } diff --git a/lib/stencil-init.js b/lib/stencil-init.js index b0e11fdc..fe9eceab 100644 --- a/lib/stencil-init.js +++ b/lib/stencil-init.js @@ -38,7 +38,7 @@ class StencilInit { const questions = this.getQuestions(defaultAnswers, cliOptions); const answers = await this.askQuestions(questions); const updatedStencilConfig = this.applyAnswers(oldStencilConfig, answers, cliOptions); - this._stencilConfigManager.saveStencilConfig(updatedStencilConfig); + await this._stencilConfigManager.save(updatedStencilConfig); this._logger.log( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, @@ -52,10 +52,10 @@ class StencilInit { let parsedConfig; try { - parsedConfig = await this._stencilConfigManager.readStencilConfig(true); + parsedConfig = await this._stencilConfigManager.read(true, true); } catch (err) { this._logger.error( - 'Detected a broken .stencil file:\n', + 'Detected a broken stencil-cli config:\n', err, '\nThe file will be rewritten with your answers', ); diff --git a/lib/stencil-init.spec.js b/lib/stencil-init.spec.js index afb27344..bcd710d3 100644 --- a/lib/stencil-init.spec.js +++ b/lib/stencil-init.spec.js @@ -1,5 +1,4 @@ const _ = require('lodash'); -const fs = require('fs'); const inquirerModule = require('inquirer'); const StencilInit = require('./stencil-init'); @@ -78,13 +77,14 @@ describe('StencilInit integration tests', () => { const stencilConfigManager = new StencilConfigManager({ themePath: './test/_mocks/themes/valid/', }); - const saveStencilConfigSpy = jest.spyOn(stencilConfigManager, 'saveStencilConfig'); + const saveStencilConfigStub = jest + .spyOn(stencilConfigManager, 'save') + .mockImplementation(jest.fn()); const inquirerPromptStub = jest .spyOn(inquirerModule, 'prompt') .mockReturnValue(answers); const consoleErrorStub = jest.spyOn(console, 'error').mockImplementation(jest.fn()); const consoleLogStub = jest.spyOn(console, 'log').mockImplementation(jest.fn()); - jest.spyOn(fs, 'writeFileSync').mockImplementation(jest.fn()); // Test with real entities, just some methods stubbed const instance = new StencilInit({ @@ -97,9 +97,9 @@ describe('StencilInit integration tests', () => { expect(inquirerPromptStub).toHaveBeenCalledTimes(1); expect(consoleErrorStub).toHaveBeenCalledTimes(0); expect(consoleLogStub).toHaveBeenCalledTimes(1); - expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigStub).toHaveBeenCalledTimes(1); - expect(saveStencilConfigSpy).toHaveBeenCalledWith(expectedResult); + expect(saveStencilConfigStub).toHaveBeenCalledWith(expectedResult); expect(consoleLogStub).toHaveBeenCalledWith( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, ); @@ -114,11 +114,12 @@ describe('StencilInit integration tests', () => { const stencilConfigManager = new StencilConfigManager({ themePath: './test/_mocks/themes/valid/', }); - const saveStencilConfigSpy = jest.spyOn(stencilConfigManager, 'saveStencilConfig'); + const saveStencilConfigStub = jest + .spyOn(stencilConfigManager, 'save') + .mockImplementation(jest.fn()); const inquirerPromptStub = jest.spyOn(inquirerModule, 'prompt').mockReturnValue({}); const consoleErrorStub = jest.spyOn(console, 'error').mockImplementation(jest.fn()); const consoleLogStub = jest.spyOn(console, 'log').mockImplementation(jest.fn()); - jest.spyOn(fs, 'writeFileSync').mockImplementation(jest.fn()); // Test with real entities, just some methods stubbed const instance = new StencilInit({ @@ -131,9 +132,9 @@ describe('StencilInit integration tests', () => { expect(inquirerPromptStub).toHaveBeenCalledTimes(0); expect(consoleErrorStub).toHaveBeenCalledTimes(0); expect(consoleLogStub).toHaveBeenCalledTimes(1); - expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); + expect(saveStencilConfigStub).toHaveBeenCalledTimes(1); - expect(saveStencilConfigSpy).toHaveBeenCalledWith(expectedResult); + expect(saveStencilConfigStub).toHaveBeenCalledWith(expectedResult); expect(consoleLogStub).toHaveBeenCalledWith( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, ); @@ -152,8 +153,8 @@ describe('StencilInit unit tests', () => { prompt: jest.fn().mockReturnValue(getAnswers()), }); const getStencilConfigManagerStub = () => ({ - readStencilConfig: jest.fn().mockReturnValue(getStencilConfig()), - saveStencilConfig: jest.fn(), + read: jest.fn().mockReturnValue(getStencilConfig()), + save: jest.fn(), }); const getServerConfigStub = () => ({ get: jest.fn( @@ -202,7 +203,7 @@ describe('StencilInit unit tests', () => { it("should return an empty config if the file doesn't exist", async () => { const loggerStub = getLoggerStub(); const stencilConfigManagerStub = getStencilConfigManagerStub(); - stencilConfigManagerStub.readStencilConfig.mockReturnValue(null); + stencilConfigManagerStub.read.mockReturnValue(null); const { instance } = createStencilInitInstance({ stencilConfigManager: stencilConfigManagerStub, @@ -210,8 +211,8 @@ describe('StencilInit unit tests', () => { }); const res = await instance.readStencilConfig(dotStencilFilePath); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(stencilConfigManagerStub.read).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.read).toHaveBeenCalledWith(true, true); expect(loggerStub.error).toHaveBeenCalledTimes(0); expect(res).toEqual({}); @@ -221,7 +222,7 @@ describe('StencilInit unit tests', () => { const parsedConfig = getStencilConfig(); const stencilConfigManagerStub = getStencilConfigManagerStub(); const loggerStub = getLoggerStub(); - stencilConfigManagerStub.readStencilConfig.mockReturnValue(parsedConfig); + stencilConfigManagerStub.read.mockReturnValue(parsedConfig); const { instance } = createStencilInitInstance({ stencilConfigManager: stencilConfigManagerStub, @@ -229,8 +230,8 @@ describe('StencilInit unit tests', () => { }); const res = await instance.readStencilConfig(dotStencilFilePath); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(stencilConfigManagerStub.read).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.read).toHaveBeenCalledWith(true, true); expect(loggerStub.error).toHaveBeenCalledTimes(0); expect(res).toEqual(parsedConfig); @@ -240,7 +241,7 @@ describe('StencilInit unit tests', () => { const thrownError = new Error('invalid file'); const loggerStub = getLoggerStub(); const stencilConfigManagerStub = getStencilConfigManagerStub(); - stencilConfigManagerStub.readStencilConfig.mockRejectedValue(thrownError); + stencilConfigManagerStub.read.mockRejectedValue(thrownError); const { instance } = createStencilInitInstance({ stencilConfigManager: stencilConfigManagerStub, @@ -248,8 +249,8 @@ describe('StencilInit unit tests', () => { }); const res = await instance.readStencilConfig(dotStencilFilePath); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); - expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(stencilConfigManagerStub.read).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.read).toHaveBeenCalledWith(true, true); expect(loggerStub.error).toHaveBeenCalledTimes(1); expect(res).toEqual({}); diff --git a/lib/stencil-push.utils.js b/lib/stencil-push.utils.js index b91aa22b..86bc6226 100644 --- a/lib/stencil-push.utils.js +++ b/lib/stencil-push.utils.js @@ -32,7 +32,7 @@ function validateOptions(options = {}, fields = []) { utils.readStencilConfigFile = async (options) => { try { - const config = await stencilConfigManager.readStencilConfig(); + const config = await stencilConfigManager.read(); return { ...options, config }; } catch (err) { err.name = 'StencilConfigReadError'; diff --git a/lib/stencil-start.js b/lib/stencil-start.js index a635e170..9c69b752 100755 --- a/lib/stencil-start.js +++ b/lib/stencil-start.js @@ -49,12 +49,12 @@ class StencilStart { await this._themeConfigManager.setVariationByName(cliOptions.variation); } - const initialStencilConfig = await this.readStencilConfig(); + const initialStencilConfig = await this._stencilConfigManager.read(); // Use initial (before updates) port for BrowserSync const browserSyncPort = initialStencilConfig.port; const storeInfoFromAPI = await this.runAPICheck(initialStencilConfig, stencilCliVersion); - const updatedStencilConfig = this.mergeStencilConfigData( + const updatedStencilConfig = this.updateStencilConfig( initialStencilConfig, storeInfoFromAPI, ); @@ -135,24 +135,7 @@ class StencilStart { return payload; } - /** - * @returns {Promise<{ data: object, filePath: string }>} - */ - async readStencilConfig() { - const parsedConfig = await this._stencilConfigManager.readStencilConfig(); - - if (!parsedConfig.normalStoreUrl || !parsedConfig.customLayouts) { - throw new Error( - 'Error: Your stencil config is outdated. Please run'.red + - ' $ stencil init'.cyan + - ' again.'.red, - ); - } - - return parsedConfig; - } - - mergeStencilConfigData(stencilConfig, storeInfoFromAPI) { + updateStencilConfig(stencilConfig, storeInfoFromAPI) { return { ...stencilConfig, storeUrl: storeInfoFromAPI.sslUrl, @@ -282,11 +265,18 @@ class StencilStart { * @returns {string} */ getStartUpInfo(stencilConfig) { + const { + configPath, + secretsPath, + configFileName, + secretsFileName, + } = this._stencilConfigManager; let information = '\n'; information += '-----------------Startup Information-------------\n'.gray; information += '\n'; - information += `.stencil location: ${this._stencilConfigManager.configPath.cyan}\n`; + information += `${configFileName} location: ${configPath.cyan}\n`; + information += `${secretsFileName} location: ${secretsPath.cyan}\n`; information += `config.json location: ${this._themeConfigManager.configPath.cyan}\n`; information += `Store URL: ${stencilConfig.normalStoreUrl.cyan}\n`; diff --git a/test/_mocks/themes/valid/.stencil b/test/_mocks/themes/valid/.stencil deleted file mode 100644 index a3425bde..00000000 --- a/test/_mocks/themes/valid/.stencil +++ /dev/null @@ -1,5 +0,0 @@ -{ - "normalStoreUrl": "https://www.example.com", - "port": 4000, - "accessToken": "accessTokenValue" -} diff --git a/test/_mocks/themes/valid/config.stencil.json b/test/_mocks/themes/valid/config.stencil.json new file mode 100644 index 00000000..0b7608c9 --- /dev/null +++ b/test/_mocks/themes/valid/config.stencil.json @@ -0,0 +1,10 @@ +{ + "customLayouts": { + "brand": {}, + "category": {}, + "page": {}, + "product": {} + }, + "normalStoreUrl": "https://url-from-answers.mybigcommerce.com", + "port": 3003 +} \ No newline at end of file diff --git a/test/_mocks/themes/valid/secrets.stencil.json b/test/_mocks/themes/valid/secrets.stencil.json new file mode 100644 index 00000000..6cf81488 --- /dev/null +++ b/test/_mocks/themes/valid/secrets.stencil.json @@ -0,0 +1,3 @@ +{ + "accessToken": "accessToken_from_answers" +}