From 7b371e8b74368ead0c3e773f9f403d80d50ce778 Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Wed, 28 Oct 2020 12:35:52 +0200 Subject: [PATCH 1/3] refactor: (strf-8747) move code dealing with .stencil file into a separate class --- bin/stencil-download.js | 3 +- bin/stencil-init.js | 16 +- bin/stencil-pull.js | 3 +- bin/stencil-push.js | 3 +- bin/stencil-start.js | 6 +- constants.js | 5 - lib/BuildConfigManager.js | 8 +- lib/StencilConfigManager.js | 43 +++ lib/release/release.js | 36 +-- lib/stencil-init.js | 68 ++--- lib/stencil-init.spec.js | 257 ++++++++---------- lib/stencil-push.spec.js | 59 ---- lib/stencil-push.utils.js | 13 +- lib/stencil-push.utils.spec.js | 33 ++- lib/stencil-start.js | 167 ++++++------ lib/stencil-start.spec.js | 3 + test/_mocks/MockDB.js | 17 -- .../valid/.stencil} | 2 +- 18 files changed, 342 insertions(+), 400 deletions(-) create mode 100644 lib/StencilConfigManager.js delete mode 100644 lib/stencil-push.spec.js delete mode 100644 test/_mocks/MockDB.js rename test/_mocks/{bin/dotStencilFile.json => themes/valid/.stencil} (65%) diff --git a/bin/stencil-download.js b/bin/stencil-download.js index 6e2bd3d1..009d9586 100644 --- a/bin/stencil-download.js +++ b/bin/stencil-download.js @@ -4,7 +4,7 @@ require('colors'); const inquirer = require('inquirer'); const program = require('../lib/commander'); -const { API_HOST, PACKAGE_INFO, DOT_STENCIL_FILE_PATH } = require('../constants'); +const { API_HOST, PACKAGE_INFO } = require('../constants'); const stencilDownload = require('../lib/stencil-download'); const { checkNodeVersion } = require('../lib/cliCommon'); const { printCliResultErrorAndExit } = require('../lib/cliCommon'); @@ -22,7 +22,6 @@ checkNodeVersion(); const cliOptions = program.opts(); const extraExclude = cliOptions.exclude ? [cliOptions.exclude] : []; const options = { - dotStencilFilePath: DOT_STENCIL_FILE_PATH, exclude: ['parsed', 'manifest.json', ...extraExclude], apiHost: cliOptions.host || API_HOST, channelId: cliOptions.channel_id || 1, diff --git a/bin/stencil-init.js b/bin/stencil-init.js index 7c8dbd97..bd896248 100755 --- a/bin/stencil-init.js +++ b/bin/stencil-init.js @@ -3,8 +3,8 @@ const program = require('../lib/commander'); const StencilInit = require('../lib/stencil-init'); -const { DOT_STENCIL_FILE_PATH, PACKAGE_INFO } = require('../constants'); -const { checkNodeVersion } = require('../lib/cliCommon'); +const { PACKAGE_INFO } = require('../constants'); +const { checkNodeVersion, printCliResultErrorAndExit } = require('../lib/cliCommon'); program .version(PACKAGE_INFO.version) @@ -17,8 +17,10 @@ checkNodeVersion(); const cliOptions = program.opts(); -new StencilInit().run(DOT_STENCIL_FILE_PATH, { - normalStoreUrl: cliOptions.url, - accessToken: cliOptions.token, - port: cliOptions.port, -}); +new StencilInit() + .run({ + normalStoreUrl: cliOptions.url, + accessToken: cliOptions.token, + port: cliOptions.port, + }) + .catch(printCliResultErrorAndExit); diff --git a/bin/stencil-pull.js b/bin/stencil-pull.js index b25e5661..475b068e 100755 --- a/bin/stencil-pull.js +++ b/bin/stencil-pull.js @@ -2,7 +2,7 @@ require('colors'); -const { DOT_STENCIL_FILE_PATH, PACKAGE_INFO, API_HOST } = require('../constants'); +const { PACKAGE_INFO, API_HOST } = require('../constants'); const program = require('../lib/commander'); const stencilPull = require('../lib/stencil-pull'); const { checkNodeVersion } = require('../lib/cliCommon'); @@ -28,7 +28,6 @@ checkNodeVersion(); const cliOptions = program.opts(); const options = { - dotStencilFilePath: DOT_STENCIL_FILE_PATH, apiHost: cliOptions.host || API_HOST, saveConfigName: cliOptions.filename, channelId: cliOptions.channel_id || 1, diff --git a/bin/stencil-push.js b/bin/stencil-push.js index 199ee789..f9b63067 100755 --- a/bin/stencil-push.js +++ b/bin/stencil-push.js @@ -1,7 +1,7 @@ #!/usr/bin/env node require('colors'); -const { DOT_STENCIL_FILE_PATH, PACKAGE_INFO, API_HOST } = require('../constants'); +const { PACKAGE_INFO, API_HOST } = require('../constants'); const program = require('../lib/commander'); const stencilPush = require('../lib/stencil-push'); const { checkNodeVersion } = require('../lib/cliCommon'); @@ -20,7 +20,6 @@ checkNodeVersion(); const cliOptions = program.opts(); const options = { - dotStencilFilePath: DOT_STENCIL_FILE_PATH, apiHost: cliOptions.host || API_HOST, bundleZipPath: cliOptions.file, activate: cliOptions.activate, diff --git a/bin/stencil-start.js b/bin/stencil-start.js index cc83c03c..5ceea765 100755 --- a/bin/stencil-start.js +++ b/bin/stencil-start.js @@ -1,7 +1,7 @@ #!/usr/bin/env node require('colors'); -const { PACKAGE_INFO, DOT_STENCIL_FILE_PATH } = require('../constants'); +const { PACKAGE_INFO } = require('../constants'); const program = require('../lib/commander'); const StencilStart = require('../lib/stencil-start'); const { printCliResultErrorAndExit } = require('../lib/cliCommon'); @@ -20,6 +20,4 @@ program ) .parse(process.argv); -new StencilStart() - .run(program.opts(), DOT_STENCIL_FILE_PATH, PACKAGE_INFO.version) - .catch(printCliResultErrorAndExit); +new StencilStart().run(program.opts(), PACKAGE_INFO.version).catch(printCliResultErrorAndExit); diff --git a/constants.js b/constants.js index e98f7bb0..987501f1 100644 --- a/constants.js +++ b/constants.js @@ -1,5 +1,3 @@ -const path = require('path'); - /// ////////////////////////////////////// Stencil CLI ///////////////////////////////////// /// const PACKAGE_INFO = require('./package.json'); @@ -8,8 +6,6 @@ const PACKAGE_INFO = require('./package.json'); const THEME_PATH = process.cwd(); -const DOT_STENCIL_FILE_PATH = path.join(THEME_PATH, '.stencil'); - const DEFAULT_CUSTOM_LAYOUTS_CONFIG = { brand: {}, category: {}, @@ -24,7 +20,6 @@ const API_HOST = 'https://api.bigcommerce.com'; module.exports = { PACKAGE_INFO, THEME_PATH, - DOT_STENCIL_FILE_PATH, DEFAULT_CUSTOM_LAYOUTS_CONFIG, API_HOST, }; diff --git a/lib/BuildConfigManager.js b/lib/BuildConfigManager.js index 79b6bfba..8687607c 100644 --- a/lib/BuildConfigManager.js +++ b/lib/BuildConfigManager.js @@ -3,12 +3,14 @@ const { fork } = require('child_process'); const path = require('path'); const fsModule = require('fs'); +const { THEME_PATH } = require('../constants'); + class BuildConfigManager { - constructor({ workDir = process.cwd(), fs = fsModule } = {}) { - this.CONFIG_FILE_NAME = 'stencil.conf.js'; + constructor({ workDir = THEME_PATH, fs = fsModule } = {}) { + this.configFileName = 'stencil.conf.js'; this._workDir = workDir; - this._buildConfigPath = path.join(workDir, this.CONFIG_FILE_NAME); + this._buildConfigPath = path.join(workDir, this.configFileName); this._fs = fs; this._onReadyCallbacks = []; this._worker = null; diff --git a/lib/StencilConfigManager.js b/lib/StencilConfigManager.js new file mode 100644 index 00000000..beaa84d7 --- /dev/null +++ b/lib/StencilConfigManager.js @@ -0,0 +1,43 @@ +require('colors'); +const fsModule = require('fs'); +const path = require('path'); + +const fsUtilsModule = require('./utils/fsUtils'); +const { THEME_PATH } = require('../constants'); + +class StencilConfigManager { + constructor({ themePath = THEME_PATH, fs = fsModule, fsUtils = fsUtilsModule } = {}) { + this.configFileName = '.stencil'; + + this.themePath = themePath; + this.configPath = path.join(themePath, this.configFileName); + + this._fs = fs; + this._fsUtils = fsUtils; + } + + /** + * @returns {object|null} + * @param {boolean} ignoreFileNotExists + */ + async readStencilConfig(ignoreFileNotExists = false) { + if (this._fs.existsSync(this.configPath)) { + return this._fsUtils.parseJsonFile(this.configPath); + } + + if (ignoreFileNotExists) { + return null; + } + + throw new Error('Please run'.red + ' $ stencil init'.cyan + ' first.'.red); + } + + /** + * @param {object} config + */ + saveStencilConfig(config) { + this._fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); + } +} + +module.exports = StencilConfigManager; diff --git a/lib/release/release.js b/lib/release/release.js index f1479b76..72f9849f 100644 --- a/lib/release/release.js +++ b/lib/release/release.js @@ -4,32 +4,28 @@ const fs = require('fs'); const path = require('path'); const { Octokit } = require('@octokit/rest'); const simpleGit = require('simple-git'); + const ThemeConfig = require('../theme-config'); const askQuestions = require('./questions'); const Bundle = require('../stencil-bundle'); +const StencilConfigManager = require('../StencilConfigManager'); const { parseJsonFile } = require('../utils/fsUtils'); -const { THEME_PATH, DOT_STENCIL_FILE_PATH } = require('../../constants'); +const { THEME_PATH } = require('../../constants'); const git = simpleGit(THEME_PATH); -const themeConfig = ThemeConfig.getInstance(THEME_PATH); +const themeConfigManager = ThemeConfig.getInstance(THEME_PATH); +const stencilConfigManager = new StencilConfigManager(); async function saveGithubToken(githubToken) { - let data = {}; - - if (fs.existsSync(DOT_STENCIL_FILE_PATH)) { - data = await parseJsonFile(DOT_STENCIL_FILE_PATH); - } + const data = (await stencilConfigManager.readStencilConfig(true)) || {}; data.githubToken = githubToken; - await fs.promises.writeFile(DOT_STENCIL_FILE_PATH, `${JSON.stringify(data, null, 2)}\n`); + + await this.stencilConfigManager.saveStencilConfig(data); } async function getGithubToken() { - let data = {}; - - if (fs.existsSync(DOT_STENCIL_FILE_PATH)) { - data = await parseJsonFile(DOT_STENCIL_FILE_PATH); - } + const data = (await stencilConfigManager.readStencilConfig(true)) || {}; return data.githubToken; } @@ -58,7 +54,7 @@ async function createGithubRelease(commit, version, changelog, remote, bundlePat console.log('Creating Github Release...'); const release = await github.repos.createRelease(releaseParams); - const themeName = await themeConfig.getName(); + const themeName = await themeConfigManager.getName(); const uploadParams = { release_id: release.data.id, @@ -108,12 +104,12 @@ async function parseChangelog(version, date) { } async function bundleTheme() { - const rawConfig = await themeConfig.getRawConfig(); + const rawConfig = await themeConfigManager.getRawConfig(); const bundleOptions = { dest: os.tmpdir(), name: uuid(), }; - const bundle = new Bundle(THEME_PATH, themeConfig, rawConfig, bundleOptions); + const bundle = new Bundle(THEME_PATH, themeConfigManager, rawConfig, bundleOptions); return bundle.initBundle(); } @@ -206,7 +202,7 @@ async function doRelease(options) { // Update changelog and get text for release notes const changelog = await parseChangelog(options.version, options.date); - await bumpJsonFileVersion(themeConfig.configPath, options.version); + await bumpJsonFileVersion(themeConfigManager.configPath, options.version); await bumpJsonFileVersion(path.join(THEME_PATH, 'package.json'), options.version); const bundlePath = await bundleTheme(); @@ -236,7 +232,11 @@ async function run() { } try { - const answers = await askQuestions(themeConfig, await getGithubToken(), gitData.remotes); + const answers = await askQuestions( + themeConfigManager, + await getGithubToken(), + gitData.remotes, + ); await saveGithubToken(answers.githubToken); diff --git a/lib/stencil-init.js b/lib/stencil-init.js index d2bcb696..b0e11fdc 100644 --- a/lib/stencil-init.js +++ b/lib/stencil-init.js @@ -1,75 +1,67 @@ require('colors'); -const fsModule = require('fs'); const inquirerModule = require('inquirer'); const serverConfigModule = require('../server/config'); -const jsonLintModule = require('./json-lint'); +const StencilConfigManager = require('./StencilConfigManager'); const { DEFAULT_CUSTOM_LAYOUTS_CONFIG } = require('../constants'); class StencilInit { /** * @param inquirer - * @param jsonLint - * @param fs + * @param stencilConfigManager * @param serverConfig * @param logger */ constructor({ inquirer = inquirerModule, - jsonLint = jsonLintModule, - fs = fsModule, + stencilConfigManager = new StencilConfigManager(), serverConfig = serverConfigModule, logger = console, } = {}) { - this.inquirer = inquirer; - this.jsonLint = jsonLint; - this.logger = logger; - this.fs = fs; - this.serverConfig = serverConfig; + this._inquirer = inquirer; + this._stencilConfigManager = stencilConfigManager; + this._serverConfig = serverConfig; + this._logger = logger; } /** - * @param {string} dotStencilFilePath * @param {object} cliOptions * @param {string} cliOptions.normalStoreUrl * @param {string} cliOptions.accessToken * @param {number} cliOptions.port * @returns {Promise} */ - async run(dotStencilFilePath, cliOptions = {}) { - const oldStencilConfig = this.readStencilConfig(dotStencilFilePath); + async run(cliOptions = {}) { + const oldStencilConfig = await this.readStencilConfig(); const defaultAnswers = this.getDefaultAnswers(oldStencilConfig); const questions = this.getQuestions(defaultAnswers, cliOptions); const answers = await this.askQuestions(questions); const updatedStencilConfig = this.applyAnswers(oldStencilConfig, answers, cliOptions); - this.saveStencilConfig(updatedStencilConfig, dotStencilFilePath); + this._stencilConfigManager.saveStencilConfig(updatedStencilConfig); - this.logger.log( + this._logger.log( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, ); } /** - * @param {string} dotStencilFilePath * @returns {object} */ - readStencilConfig(dotStencilFilePath) { - if (this.fs.existsSync(dotStencilFilePath)) { - const dotStencilFile = this.fs.readFileSync(dotStencilFilePath, { encoding: 'utf-8' }); - try { - // We use jsonLint.parse instead of JSON.parse because jsonLint throws errors with better explanations what is wrong - return this.jsonLint.parse(dotStencilFile, dotStencilFilePath); - } catch (err) { - this.logger.error( - 'Detected a broken .stencil file:\n', - err, - '\nThe file will be rewritten with your answers', - ); - } + async readStencilConfig() { + let parsedConfig; + + try { + parsedConfig = await this._stencilConfigManager.readStencilConfig(true); + } catch (err) { + this._logger.error( + 'Detected a broken .stencil file:\n', + err, + '\nThe file will be rewritten with your answers', + ); } - return {}; + return parsedConfig || {}; } /** @@ -80,7 +72,7 @@ class StencilInit { return { normalStoreUrl: stencilConfig.normalStoreUrl, accessToken: stencilConfig.accessToken, - port: stencilConfig.port || this.serverConfig.get('/server/port'), + port: stencilConfig.port || this._serverConfig.get('/server/port'), }; } @@ -138,13 +130,13 @@ class StencilInit { * @returns {Promise} */ async askQuestions(questions) { - return questions.length ? this.inquirer.prompt(questions) : {}; + return questions.length ? this._inquirer.prompt(questions) : {}; } /** * @param {object} stencilConfig * @param {object} answers - * @param {{port: (number), url: (string), token: (string)}} cliOptions + * @param {object} cliOptions * @returns {object} */ applyAnswers(stencilConfig, answers, cliOptions) { @@ -155,14 +147,6 @@ class StencilInit { ...answers, }; } - - /** - * @param {object} config - * @param {string} path - */ - saveStencilConfig(config, path) { - this.fs.writeFileSync(path, JSON.stringify(config, null, 2)); - } } module.exports = StencilInit; diff --git a/lib/stencil-init.spec.js b/lib/stencil-init.spec.js index 9657546e..afb27344 100644 --- a/lib/stencil-init.spec.js +++ b/lib/stencil-init.spec.js @@ -1,10 +1,9 @@ const _ = require('lodash'); const fs = require('fs'); -const inquirer = require('inquirer'); +const inquirerModule = require('inquirer'); -const jsonLint = require('./json-lint'); -const serverConfig = require('../server/config'); const StencilInit = require('./stencil-init'); +const StencilConfigManager = require('./StencilConfigManager'); const { DEFAULT_CUSTOM_LAYOUTS_CONFIG } = require('../constants'); const { assertNoMutations } = require('../test/assertions/assertNoMutations'); @@ -71,72 +70,70 @@ afterEach(() => jest.restoreAllMocks()); describe('StencilInit integration tests', () => { describe('run', () => { it('using cli prompts, should perform all the actions, save the result and inform the user about the successful finish', async () => { - const dotStencilFilePath = './test/_mocks/bin/dotStencilFile.json'; const answers = getAnswers(); - const expectedResult = JSON.stringify( - { customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, ...answers }, - null, - 2, - ); - const fsWriteFileSyncStub = jest - .spyOn(fs, 'writeFileSync') - .mockImplementation(jest.fn()); - const inquirerPromptStub = jest.spyOn(inquirer, 'prompt').mockReturnValue(answers); + const expectedResult = { + customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, + ...answers, + }; + const stencilConfigManager = new StencilConfigManager({ + themePath: './test/_mocks/themes/valid/', + }); + const saveStencilConfigSpy = jest.spyOn(stencilConfigManager, 'saveStencilConfig'); + 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({ - inquirer, - jsonLint, - fs, - serverConfig, + inquirer: inquirerModule, + stencilConfigManager, logger: console, }); - await instance.run(dotStencilFilePath); + await instance.run(); - expect(fsWriteFileSyncStub).toHaveBeenCalledTimes(1); expect(inquirerPromptStub).toHaveBeenCalledTimes(1); expect(consoleErrorStub).toHaveBeenCalledTimes(0); expect(consoleLogStub).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); - expect(fsWriteFileSyncStub).toHaveBeenCalledWith(dotStencilFilePath, expectedResult); + expect(saveStencilConfigSpy).toHaveBeenCalledWith(expectedResult); expect(consoleLogStub).toHaveBeenCalledWith( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, ); }); it('using cli options, should perform all the actions, save the result and inform the user about the successful finish', async () => { - const dotStencilFilePath = './test/_mocks/bin/dotStencilFile.json'; const cliOptions = getCliOptions(); - const expectedResult = JSON.stringify( - { customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, ...cliOptions }, - null, - 2, - ); - const fsWriteFileSyncStub = jest - .spyOn(fs, 'writeFileSync') - .mockImplementation(jest.fn()); - const inquirerPromptStub = jest.spyOn(inquirer, 'prompt').mockReturnValue({}); + const expectedResult = { + customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, + ...cliOptions, + }; + const stencilConfigManager = new StencilConfigManager({ + themePath: './test/_mocks/themes/valid/', + }); + const saveStencilConfigSpy = jest.spyOn(stencilConfigManager, 'saveStencilConfig'); + 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({ - inquirer, - jsonLint, - fs, - serverConfig, + inquirer: inquirerModule, + stencilConfigManager, logger: console, }); - await instance.run(dotStencilFilePath, cliOptions); + await instance.run(cliOptions); - expect(fsWriteFileSyncStub).toHaveBeenCalledTimes(1); expect(inquirerPromptStub).toHaveBeenCalledTimes(0); expect(consoleErrorStub).toHaveBeenCalledTimes(0); expect(consoleLogStub).toHaveBeenCalledTimes(1); + expect(saveStencilConfigSpy).toHaveBeenCalledTimes(1); - expect(fsWriteFileSyncStub).toHaveBeenCalledWith(dotStencilFilePath, expectedResult); + expect(saveStencilConfigSpy).toHaveBeenCalledWith(expectedResult); expect(consoleLogStub).toHaveBeenCalledWith( 'You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan, ); @@ -146,48 +143,46 @@ describe('StencilInit integration tests', () => { describe('StencilInit unit tests', () => { const serverConfigPort = 3000; - const dotStencilFilePath = '/some/test/path/dotStencilFile.json'; - let consoleStub; - let inquirerStub; - let jsonLintStub; - let fsStub; - let serverConfigStub; - let getStencilInitInstance; - - beforeEach(() => { - consoleStub = { - log: jest.fn(), - error: jest.fn(), - }; - inquirerStub = { - prompt: jest.fn().mockReturnValue(getAnswers()), - }; - jsonLintStub = { - parse: jest.fn().mockReturnValue(getStencilConfig()), - }; - fsStub = { - existsSync: jest.fn(), - readFileSync: jest.fn(), - writeFileSync: jest.fn(), - }; - serverConfigStub = { - get: jest.fn( - (prop) => - ({ - '/server/port': serverConfigPort, - }[prop]), - ), + const dotStencilFilePath = '/some/test/path/.stencil'; + const getLoggerStub = () => ({ + log: jest.fn(), + error: jest.fn(), + }); + const getInquirerStub = () => ({ + prompt: jest.fn().mockReturnValue(getAnswers()), + }); + const getStencilConfigManagerStub = () => ({ + readStencilConfig: jest.fn().mockReturnValue(getStencilConfig()), + saveStencilConfig: jest.fn(), + }); + const getServerConfigStub = () => ({ + get: jest.fn( + (prop) => + ({ + '/server/port': serverConfigPort, + }[prop]), + ), + }); + + const createStencilInitInstance = ({ + inquirer, + stencilConfigManager, + serverConfig, + logger, + } = {}) => { + const passedArgs = { + inquirer: inquirer || getInquirerStub(), + stencilConfigManager: stencilConfigManager || getStencilConfigManagerStub(), + serverConfig: serverConfig || getServerConfigStub(), + logger: logger || getLoggerStub(), }; + const instance = new StencilInit(passedArgs); - getStencilInitInstance = () => - new StencilInit({ - inquirer: inquirerStub, - jsonLint: jsonLintStub, - fs: fsStub, - serverConfig: serverConfigStub, - logger: consoleStub, - }); - }); + return { + passedArgs, + instance, + }; + }; describe('constructor', () => { it('should create an instance of StencilInit without options parameters passed', async () => { @@ -197,7 +192,7 @@ describe('StencilInit unit tests', () => { }); it('should create an instance of StencilInit with options parameters passed', async () => { - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); expect(instance).toBeInstanceOf(StencilInit); }); @@ -205,61 +200,57 @@ describe('StencilInit unit tests', () => { describe('readStencilConfig', () => { it("should return an empty config if the file doesn't exist", async () => { - const instance = getStencilInitInstance(); - fsStub.existsSync.mockReturnValue(false); + const loggerStub = getLoggerStub(); + const stencilConfigManagerStub = getStencilConfigManagerStub(); + stencilConfigManagerStub.readStencilConfig.mockReturnValue(null); - const res = instance.readStencilConfig(dotStencilFilePath); + const { instance } = createStencilInitInstance({ + stencilConfigManager: stencilConfigManagerStub, + logger: loggerStub, + }); + const res = await instance.readStencilConfig(dotStencilFilePath); + + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(loggerStub.error).toHaveBeenCalledTimes(0); - expect(fsStub.existsSync).toHaveBeenCalledTimes(1); expect(res).toEqual({}); }); it('should read the file and return parsed results if the file exists and it is valid', async () => { const parsedConfig = getStencilConfig(); - const serializedConfig = JSON.stringify(parsedConfig, null, 2); - const instance = getStencilInitInstance(); - fsStub.existsSync.mockReturnValue(true); - fsStub.readFileSync.mockReturnValue(serializedConfig); - jsonLintStub.parse.mockReturnValue(parsedConfig); - - const res = instance.readStencilConfig(dotStencilFilePath); - - expect(fsStub.existsSync).toHaveBeenCalledTimes(1); - expect(fsStub.readFileSync).toHaveBeenCalledTimes(1); - expect(jsonLintStub.parse).toHaveBeenCalledTimes(1); - expect(consoleStub.error).toHaveBeenCalledTimes(0); - - expect(fsStub.existsSync).toHaveBeenCalledWith(dotStencilFilePath); - expect(fsStub.readFileSync).toHaveBeenCalledWith(dotStencilFilePath, { - encoding: 'utf-8', + const stencilConfigManagerStub = getStencilConfigManagerStub(); + const loggerStub = getLoggerStub(); + stencilConfigManagerStub.readStencilConfig.mockReturnValue(parsedConfig); + + const { instance } = createStencilInitInstance({ + stencilConfigManager: stencilConfigManagerStub, + logger: loggerStub, }); - expect(jsonLintStub.parse).toHaveBeenCalledWith(serializedConfig, dotStencilFilePath); + const res = await instance.readStencilConfig(dotStencilFilePath); + + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(loggerStub.error).toHaveBeenCalledTimes(0); expect(res).toEqual(parsedConfig); }); it('should read the file, inform the user that the file is broken and return an empty config', async () => { - const serializedConfig = '{ I am broken! }'; const thrownError = new Error('invalid file'); - const instance = getStencilInitInstance(); - fsStub.existsSync.mockReturnValue(true); - fsStub.readFileSync.mockReturnValue(serializedConfig); - jsonLintStub.parse.mockImplementation(() => { - throw thrownError; - }); - - const res = instance.readStencilConfig(dotStencilFilePath); + const loggerStub = getLoggerStub(); + const stencilConfigManagerStub = getStencilConfigManagerStub(); + stencilConfigManagerStub.readStencilConfig.mockRejectedValue(thrownError); - expect(fsStub.existsSync).toHaveBeenCalledTimes(1); - expect(fsStub.readFileSync).toHaveBeenCalledTimes(1); - expect(jsonLintStub.parse).toHaveBeenCalledTimes(1); - expect(consoleStub.error).toHaveBeenCalledTimes(1); - - expect(fsStub.existsSync).toHaveBeenCalledWith(dotStencilFilePath); - expect(fsStub.readFileSync).toHaveBeenCalledWith(dotStencilFilePath, { - encoding: 'utf-8', + const { instance } = createStencilInitInstance({ + stencilConfigManager: stencilConfigManagerStub, + logger: loggerStub, }); - expect(jsonLintStub.parse).toHaveBeenCalledWith(serializedConfig, dotStencilFilePath); + const res = await instance.readStencilConfig(dotStencilFilePath); + + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledTimes(1); + expect(stencilConfigManagerStub.readStencilConfig).toHaveBeenCalledWith(true); + expect(loggerStub.error).toHaveBeenCalledTimes(1); expect(res).toEqual({}); }); @@ -269,8 +260,8 @@ describe('StencilInit unit tests', () => { // eslint-disable-next-line jest/expect-expect it('should not mutate the passed objects', async () => { const stencilConfig = getStencilConfig(); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); await assertNoMutations([stencilConfig], () => instance.getDefaultAnswers(stencilConfig), ); @@ -278,8 +269,8 @@ describe('StencilInit unit tests', () => { it('should pick values from stencilConfig if not empty', async () => { const stencilConfig = getStencilConfig(); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); const res = instance.getDefaultAnswers(stencilConfig); expect(res.normalStoreUrl).toEqual(stencilConfig.normalStoreUrl); @@ -289,8 +280,8 @@ describe('StencilInit unit tests', () => { it('should pick values from serverConfig if stencilConfig are empty', async () => { const stencilConfig = _.pick(getStencilConfig(), ['accessToken', 'url']); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); const res = instance.getDefaultAnswers(stencilConfig); expect(res.port).toEqual(serverConfigPort); @@ -304,8 +295,8 @@ describe('StencilInit unit tests', () => { it('should get all questions if no cli options were passed', async () => { const defaultAnswers = getAnswers(); const cliConfig = {}; - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); const res = instance.getQuestions(defaultAnswers, cliConfig); // We compare the serialized results because the objects contain functions which hinders direct comparison @@ -315,9 +306,12 @@ describe('StencilInit unit tests', () => { describe('askQuestions', () => { it('should call inquirer.prompt with correct arguments', async () => { - const instance = getStencilInitInstance(); const questions = getQuestions(); + const inquirerStub = getInquirerStub(); + const { instance } = createStencilInitInstance({ + inquirer: inquirerStub, + }); await instance.askQuestions(questions); expect(inquirerStub.prompt).toHaveBeenCalledTimes(1); @@ -334,8 +328,8 @@ describe('StencilInit unit tests', () => { it('should not mutate the passed objects', async () => { const stencilConfig = getStencilConfig(); const answers = getAnswers(); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); await assertNoMutations([stencilConfig, answers], () => instance.applyAnswers(stencilConfig, answers), ); @@ -344,8 +338,8 @@ describe('StencilInit unit tests', () => { it('should correctly merge values from the passed objects', async () => { const stencilConfig = getStencilConfig(); const answers = getAnswers(); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); const res = instance.applyAnswers(stencilConfig, answers); expect(res.normalStoreUrl).toEqual(answers.normalStoreUrl); @@ -359,8 +353,8 @@ describe('StencilInit unit tests', () => { it("should add a customLayouts property with default empty values if it's absent in stencilConfig", async () => { const stencilConfig = _.omit(getStencilConfig(), 'customLayouts'); const answers = getAnswers(); - const instance = getStencilInitInstance(); + const { instance } = createStencilInitInstance(); const res = instance.applyAnswers(stencilConfig, answers); expect(res.customLayouts).toEqual(DEFAULT_CUSTOM_LAYOUTS_CONFIG); @@ -369,17 +363,4 @@ describe('StencilInit unit tests', () => { expect(res.githubToken).toEqual(stencilConfig.githubToken); }); }); - - describe('saveStencilConfig', () => { - it('should call fs.writeFileSync with the serialized config', async () => { - const stencilConfig = getStencilConfig(); - const serializedConfig = JSON.stringify(stencilConfig, null, 2); - const instance = getStencilInitInstance(); - - instance.saveStencilConfig(stencilConfig, dotStencilFilePath); - - expect(fsStub.writeFileSync).toHaveBeenCalledTimes(1); - expect(fsStub.writeFileSync).toHaveBeenCalledWith(dotStencilFilePath, serializedConfig); - }); - }); }); diff --git a/lib/stencil-push.spec.js b/lib/stencil-push.spec.js deleted file mode 100644 index 7fb8743a..00000000 --- a/lib/stencil-push.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -const fetchMock = require('node-fetch'); -const { promisify } = require('util'); - -const stencilPush = require('./stencil-push'); -const utils = require('./stencil-push.utils.js'); -const { MockDB } = require('../test/_mocks/MockDB'); - -const mockDb = new MockDB(); - -// eslint-disable-next-line global-require,node/no-unpublished-require -jest.mock('node-fetch', () => require('fetch-mock-jest').sandbox()); - -describe('stencil push', () => { - beforeEach(() => { - fetchMock.mock('*', mockDb.data); - - const utilStub = (data) => async (options) => ({ ...options, ...data }); - - jest.spyOn(utils, 'generateBundle').mockImplementation( - utilStub({ - bundleZipPath: 'bundleZipPath', - }), - ); - jest.spyOn(utils, 'promptUserWhetherToApplyTheme').mockImplementation( - utilStub({ - applyTheme: true, - }), - ); - jest.spyOn(utils, 'promptUserForVariation').mockImplementation( - utilStub({ - variationId: 'bold', - }), - ); - }); - - afterEach(() => { - jest.restoreAllMocks(); - fetchMock.mockReset(); - mockDb.data = {}; - }); - - it('should throw an error if dotStencilFilePath is not provided', async () => { - await expect(promisify(stencilPush)({})).rejects.toThrow('dotStencilFilePath is required!'); - }); - - it('should return an error if dotStencilFilePath does not map to a file', async () => { - await expect(promisify(stencilPush)({ dotStencilFilePath: 'DNE' })).rejects.toThrow( - /ENOENT/, - ); - }); - - it('should return an error if it fails to retrieve the store hash', async () => { - const dotStencilFilePath = `${__dirname}/../test/_mocks/bin/dotStencilFile.json`; - - await expect(promisify(stencilPush)({ dotStencilFilePath })).rejects.toThrow( - 'Received empty store_hash value in the server response', - ); - }); -}); diff --git a/lib/stencil-push.utils.js b/lib/stencil-push.utils.js index e85bdbf7..b91aa22b 100644 --- a/lib/stencil-push.utils.js +++ b/lib/stencil-push.utils.js @@ -10,9 +10,10 @@ const { THEME_PATH } = require('../constants'); const Bundle = require('./stencil-bundle'); const themeApiClient = require('./theme-api-client'); const ThemeConfig = require('./theme-config'); -const { parseJsonFile } = require('./utils/fsUtils'); +const StencilConfigManager = require('./StencilConfigManager'); -const themeConfig = ThemeConfig.getInstance(THEME_PATH); +const themeConfigManager = ThemeConfig.getInstance(THEME_PATH); +const stencilConfigManager = new StencilConfigManager(); const utils = {}; const bar = new ProgressBar('Processing [:bar] :percent; ETA: :etas', { @@ -30,10 +31,8 @@ function validateOptions(options = {}, fields = []) { } utils.readStencilConfigFile = async (options) => { - validateOptions(options, ['dotStencilFilePath']); - try { - const config = await parseJsonFile(options.dotStencilFilePath); + const config = await stencilConfigManager.readStencilConfig(); return { ...options, config }; } catch (err) { err.name = 'StencilConfigReadError'; @@ -82,8 +81,8 @@ utils.generateBundle = async (options) => { const output = options.saveBundleName ? { dest: THEME_PATH, name: options.saveBundleName } : { dest: os.tmpdir(), name: uuid() }; - const rawConfig = await themeConfig.getRawConfig(); - const bundle = new Bundle(THEME_PATH, themeConfig, rawConfig, output); + const rawConfig = await themeConfigManager.getRawConfig(); + const bundle = new Bundle(THEME_PATH, themeConfigManager, rawConfig, output); try { const bundleZipPath = await bundle.initBundle(); diff --git a/lib/stencil-push.utils.spec.js b/lib/stencil-push.utils.spec.js index 7b16a742..f2f410f5 100644 --- a/lib/stencil-push.utils.spec.js +++ b/lib/stencil-push.utils.spec.js @@ -1,18 +1,16 @@ const fetchMock = require('node-fetch'); -const mockConfig = require('../test/_mocks/bin/dotStencilFile.json'); -const { MockDB } = require('../test/_mocks/MockDB'); const { getStoreHash } = require('./stencil-push.utils'); -const mockDb = new MockDB(); - // eslint-disable-next-line node/no-unpublished-require,global-require jest.mock('node-fetch', () => require('fetch-mock-jest').sandbox()); describe('stencil push utils', () => { - beforeEach(() => { - fetchMock.mock('*', mockDb.data); - }); + const mockConfig = { + normalStoreUrl: 'https://www.example.com', + port: 4000, + accessToken: 'accessTokenValue', + }; afterEach(() => { jest.restoreAllMocks(); @@ -20,16 +18,25 @@ describe('stencil push utils', () => { }); describe('.getStoreHash()', () => { - mockDb.data = { - store_hash: 'abc123', - statusCode: 200, - }; - it('should get the store hash', async () => { + const mockResponseData = { + store_hash: 'abc123', + }; + fetchMock.mock('*', mockResponseData); + const result = await getStoreHash({ config: mockConfig }); - expect(result.storeHash).toEqual(mockDb.data.store_hash); + expect(result.storeHash).toEqual(mockResponseData.store_hash); expect(result.config.normalStoreUrl).toEqual(mockConfig.normalStoreUrl); }); + + it('should return an error if it fails to retrieve the store hash', async () => { + const mockResponseData = {}; + fetchMock.mock('*', mockResponseData); + + await expect(getStoreHash({ config: mockConfig })).rejects.toThrow( + 'Received empty store_hash value in the server response', + ); + }); }); }); diff --git a/lib/stencil-start.js b/lib/stencil-start.js index 7a922200..a635e170 100755 --- a/lib/stencil-start.js +++ b/lib/stencil-start.js @@ -1,5 +1,5 @@ require('colors'); -const browserSyncInstance = require('browser-sync').create(); +const BrowserSync = require('browser-sync'); const { promisify } = require('util'); const fetchModule = require('node-fetch'); const fsModule = require('fs'); @@ -9,85 +9,70 @@ const Cycles = require('./Cycles'); const templateAssemblerModule = require('./template-assembler'); const { THEME_PATH } = require('../constants'); const Server = require('../server'); +const StencilConfigManager = require('./StencilConfigManager'); const ThemeConfig = require('./theme-config'); const BuildConfigManager = require('./BuildConfigManager'); const fsUtilsModule = require('./utils/fsUtils'); const cliCommonModule = require('./cliCommon'); -const themeConfigManagerInstance = ThemeConfig.getInstance(THEME_PATH); -const buildConfigManagerInstance = new BuildConfigManager({ fs: fsModule, workDir: THEME_PATH }); - class StencilStart { constructor({ - browserSync = browserSyncInstance, + browserSync = BrowserSync.create(), fetch = fetchModule, fs = fsModule, fsUtils = fsUtilsModule, cliCommon = cliCommonModule, - themeConfigManager = themeConfigManagerInstance, - buildConfigManger = buildConfigManagerInstance, + stencilConfigManager = new StencilConfigManager(), + themeConfigManager = ThemeConfig.getInstance(THEME_PATH), + buildConfigManger = new BuildConfigManager(), templateAssembler = templateAssemblerModule, CyclesDetector = Cycles, logger = console, } = {}) { - this.browserSync = browserSync; - this.fetch = fetch; - this.fs = fs; - this.fsUtils = fsUtils; - this.cliCommon = cliCommon; - this.themeConfigManager = themeConfigManager; - this.buildConfigManger = buildConfigManger; - this.templateAssembler = templateAssembler; - this.CyclesDetector = CyclesDetector; - this.logger = logger; + this._browserSync = browserSync; + this._fetch = fetch; + this._fs = fs; + this._fsUtils = fsUtils; + this._cliCommon = cliCommon; + this._stencilConfigManager = stencilConfigManager; + this._themeConfigManager = themeConfigManager; + this._buildConfigManger = buildConfigManger; + this._templateAssembler = templateAssembler; + this._CyclesDetector = CyclesDetector; + this._logger = logger; } - async run(cliOptions, dotStencilFilePath, stencilCliVersion) { - this.performChecks(cliOptions, dotStencilFilePath); + async run(cliOptions, stencilCliVersion) { + this.runBasicChecks(cliOptions); if (cliOptions.variation) { - await this.themeConfigManager.setVariationByName(cliOptions.variation); - } - - const stencilConfig = await this.fsUtils.parseJsonFile(dotStencilFilePath); - const browserSyncPort = stencilConfig.port; - stencilConfig.port = Number(stencilConfig.port) + 1; - - if (!stencilConfig.normalStoreUrl || !stencilConfig.customLayouts) { - throw new Error( - 'Error: Your stencil config is outdated. Please run'.red + - ' $ stencil init'.cyan + - ' again.'.red, - ); + await this._themeConfigManager.setVariationByName(cliOptions.variation); } - const storeInfoFromAPI = await this.runAPICheck(stencilConfig, stencilCliVersion); + const initialStencilConfig = await this.readStencilConfig(); + // Use initial (before updates) port for BrowserSync + const browserSyncPort = initialStencilConfig.port; - const updatedStencilConfig = { - ...stencilConfig, - storeUrl: storeInfoFromAPI.sslUrl, - normalStoreUrl: storeInfoFromAPI.baseUrl, - }; + const storeInfoFromAPI = await this.runAPICheck(initialStencilConfig, stencilCliVersion); + const updatedStencilConfig = this.mergeStencilConfigData( + initialStencilConfig, + storeInfoFromAPI, + ); await this.startLocalServer(cliOptions, updatedStencilConfig); - this.logger.log(this.getStartUpInfo(updatedStencilConfig, dotStencilFilePath)); + this._logger.log(this.getStartUpInfo(updatedStencilConfig)); await this.startBrowserSync(cliOptions, updatedStencilConfig, browserSyncPort); } /** * @param {Object} cliOptions - * @param {string} dotStencilFilePath */ - performChecks(cliOptions, dotStencilFilePath) { - this.cliCommon.checkNodeVersion(); + runBasicChecks(cliOptions) { + this._cliCommon.checkNodeVersion(); - if (!this.fs.existsSync(dotStencilFilePath)) { - throw new Error('Please run'.red + ' $ stencil init'.cyan + ' first.'.red); - } - - if (!this.fs.existsSync(this.themeConfigManager.configPath)) { + if (!this._fs.existsSync(this._themeConfigManager.configPath)) { throw new Error( 'You must have a '.red + ' config.json '.cyan + @@ -122,7 +107,7 @@ class StencilStart { } try { - const response = await this.fetch(reqUrl, { headers }); + const response = await this._fetch(reqUrl, { headers }); if (!response.ok) { throw new Error(response.statusText); } @@ -150,6 +135,32 @@ 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) { + return { + ...stencilConfig, + storeUrl: storeInfoFromAPI.sslUrl, + normalStoreUrl: storeInfoFromAPI.baseUrl, + port: Number(stencilConfig.port) + 1, + }; + } + /** * @param {Object} cliOptions * @param {Object} stencilConfig @@ -158,52 +169,53 @@ class StencilStart { async startLocalServer(cliOptions, stencilConfig) { return Server.create({ dotStencilFile: stencilConfig, - variationIndex: this.themeConfigManager.variationIndex || 0, + variationIndex: this._themeConfigManager.variationIndex || 0, useCache: cliOptions.cache, - themePath: this.themeConfigManager.themePath, + themePath: this._themeConfigManager.themePath, }); } async startBrowserSync(cliOptions, stencilConfig, browserSyncPort) { const DEFAULT_WATCH_FILES = ['/assets', '/templates', '/lang', '/.config']; const DEFAULT_WATCH_IGNORED = ['/assets/scss', '/assets/css']; - const { themePath, configPath } = this.themeConfigManager; + const { themePath, configPath } = this._themeConfigManager; + const { watchOptions } = this._buildConfigManger; // Watch sccs directory and automatically reload all css files if a file changes const stylesPath = path.join(themePath, 'assets/scss'); - this.browserSync.watch(stylesPath, (event) => { + this._browserSync.watch(stylesPath, (event) => { if (event === 'change') { - this.browserSync.reload('*.css'); + this._browserSync.reload('*.css'); } }); - this.browserSync.watch(configPath, (event) => { + this._browserSync.watch(configPath, (event) => { if (event === 'change') { - this.themeConfigManager.resetVariationSettings(); - this.browserSync.reload(); + this._themeConfigManager.resetVariationSettings(); + this._browserSync.reload(); } }); const storefrontConfigPath = path.join(themePath, '.config/storefront.json'); - this.browserSync.watch(storefrontConfigPath, (event, file) => { + this._browserSync.watch(storefrontConfigPath, (event, file) => { if (event === 'change') { - this.logger.log('storefront.json changed'); - this.browserSync.emitter.emit('storefront_config_file:changed', { + this._logger.log('storefront.json changed'); + this._browserSync.emitter.emit('storefront_config_file:changed', { event, path: file, namespace: '', }); - this.browserSync.reload(); + this._browserSync.reload(); } }); const templatesPath = path.join(themePath, 'templates'); - this.browserSync.watch(templatesPath, { ignoreInitial: true }, async () => { + this._browserSync.watch(templatesPath, { ignoreInitial: true }, async () => { try { const results = await this.assembleTemplates(templatesPath); - new this.CyclesDetector(results).detect(); + new this._CyclesDetector(results).detect(); } catch (e) { - this.logger.error(e); + this._logger.error(e); } }); @@ -212,14 +224,10 @@ class StencilStart { // convert undefined/true -> false/true const tunnel = typeof cliOptions.tunnel === 'string' ? cliOptions.tunnel : Boolean(cliOptions.tunnel); - const watchFiles = - (this.buildConfigManger.watchOptions && this.buildConfigManger.watchOptions.files) || - DEFAULT_WATCH_FILES; - const watchIgnored = - (this.buildConfigManger.watchOptions && this.buildConfigManger.watchOptions.ignored) || - DEFAULT_WATCH_IGNORED; - - this.browserSync.init({ + const watchFiles = (watchOptions && watchOptions.files) || DEFAULT_WATCH_FILES; + const watchIgnored = (watchOptions && watchOptions.ignored) || DEFAULT_WATCH_IGNORED; + + this._browserSync.init({ open: !!cliOptions.open, port: browserSyncPort, files: watchFiles.map((val) => path.join(themePath, val)), @@ -240,12 +248,12 @@ class StencilStart { // if the keys entered match the restartable value, then restart! if (normalizedData === 'rs') { - this.browserSync.reload(); + this._browserSync.reload(); } }); - if (this.buildConfigManger.development) { - this.buildConfigManger.initWorker().development(this.browserSync); + if (this._buildConfigManger.development) { + this._buildConfigManger.initWorker().development(this._browserSync); } } @@ -256,14 +264,14 @@ class StencilStart { * @returns {object[]} */ async assembleTemplates(templatesPath) { - const filesPaths = await this.fsUtils.recursiveReadDir(templatesPath, ['!*.html']); + const filesPaths = await this._fsUtils.recursiveReadDir(templatesPath, ['!*.html']); const templateNames = filesPaths.map((file) => file.replace(templatesPath + path.sep, '').replace('.html', ''), ); return Promise.all( templateNames.map(async (templateName) => - promisify(this.templateAssembler.assemble)(templatesPath, templateName), + promisify(this._templateAssembler.assemble)(templatesPath, templateName), ), ); } @@ -271,16 +279,15 @@ class StencilStart { /** * Displays information about your environment and configuration. * @param {Object} stencilConfig - * @param {string} dotStencilFilePath * @returns {string} */ - getStartUpInfo(stencilConfig, dotStencilFilePath) { + getStartUpInfo(stencilConfig) { let information = '\n'; information += '-----------------Startup Information-------------\n'.gray; information += '\n'; - information += `.stencil location: ${dotStencilFilePath.cyan}\n`; - information += `config.json location: ${this.themeConfigManager.configPath.cyan}\n`; + information += `.stencil location: ${this._stencilConfigManager.configPath.cyan}\n`; + information += `config.json location: ${this._themeConfigManager.configPath.cyan}\n`; information += `Store URL: ${stencilConfig.normalStoreUrl.cyan}\n`; if (stencilConfig.staplerUrl) { diff --git a/lib/stencil-start.spec.js b/lib/stencil-start.spec.js index 66dad2c6..7e5ea0b0 100644 --- a/lib/stencil-start.spec.js +++ b/lib/stencil-start.spec.js @@ -22,6 +22,7 @@ describe('StencilStart unit tests', () => { checkNodeVersion: jest.fn(), }); const getThemeConfigManagerStub = () => ({}); + const getStencilConfigManagerStub = () => ({}); const getBuildConfigMangerStub = () => ({}); const getTemplateAssemblerStub = () => ({}); const getCyclesDetectorConstructorStub = () => jest.fn(); @@ -36,6 +37,7 @@ describe('StencilStart unit tests', () => { fs, fsUtils, cliCommon, + stencilConfigManager, themeConfigManager, buildConfigManger, templateAssembler, @@ -48,6 +50,7 @@ describe('StencilStart unit tests', () => { fs: fs || getFsStub(), fsUtils: fsUtils || getFsUtilsStub(), cliCommon: cliCommon || getCliCommonStub(), + stencilConfigManager: stencilConfigManager || getStencilConfigManagerStub(), themeConfigManager: themeConfigManager || getThemeConfigManagerStub(), buildConfigManger: buildConfigManger || getBuildConfigMangerStub(), templateAssembler: templateAssembler || getTemplateAssemblerStub(), diff --git a/test/_mocks/MockDB.js b/test/_mocks/MockDB.js deleted file mode 100644 index 1b2f7de7..00000000 --- a/test/_mocks/MockDB.js +++ /dev/null @@ -1,17 +0,0 @@ -class MockDB { - constructor() { - this._data = {}; - } - - get data() { - return this._data; - } - - set data(data) { - this._data = data; - } -} - -module.exports = { - MockDB, -}; diff --git a/test/_mocks/bin/dotStencilFile.json b/test/_mocks/themes/valid/.stencil similarity index 65% rename from test/_mocks/bin/dotStencilFile.json rename to test/_mocks/themes/valid/.stencil index ae0cea45..a3425bde 100644 --- a/test/_mocks/bin/dotStencilFile.json +++ b/test/_mocks/themes/valid/.stencil @@ -1,5 +1,5 @@ { "normalStoreUrl": "https://www.example.com", "port": 4000, - "accessToken": "accessToken" + "accessToken": "accessTokenValue" } From 6f3d2dcaa7205b7c8c54da729c936c649d109e38 Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Thu, 29 Oct 2020 23:09:39 +0200 Subject: [PATCH 2/3] 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" +} From 9dfa78c050bda5d0d922e85eeeb257b73916d80e Mon Sep 17 00:00:00 2001 From: MaxGenash Date: Thu, 29 Oct 2020 23:12:05 +0200 Subject: [PATCH 3/3] feat: increase coverage threshold --- jest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jest.config.js b/jest.config.js index 48e35b97..6cebbfc1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,10 +7,10 @@ module.exports = { coverageDirectory: './.coverage', coverageThreshold: { global: { - branches: 33, - functions: 47, - lines: 47, - statements: 47, + branches: 40, + functions: 50, + lines: 50, + statements: 50, }, }, moduleFileExtensions: ['js', 'json', 'node'],