diff --git a/README.md b/README.md index 087f994..83dd87a 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,12 @@ sample rokudeploy.json The new release has a few breaking changes that is worth going over in order to prepare developers for what they will need to change when they choose to upgrade. ### JavaScript functions don't load config files from disk -In v3, files like `roku-deploy.json` and `bsconfig.json` would be loaded anytime a rokuDeploy function was called through the NodeJS api. This functionality has been removed in v4 so that developers have more control over when the config files are loaded. If your script needs to load the config file values, you can simply call `util.getOptionsFromJson` before calling the desired rokuDeploy function. This will default to load from `rokudeploy.json`. Here's an example: +In v3, files like `roku-deploy.json` and `bsconfig.json` would be loaded anytime a rokuDeploy function was called through the NodeJS api. This functionality has been removed in v4 so that developers have more control over when the config files are loaded. If your script needs to load the config file values, you can simply call `RokuDeploy.loadOptionsFromJson` before calling the desired rokuDeploy function. This will default to load from `rokudeploy.json`. Here's an example: ```javascript const config = { - //get the default options - ...rokuDeploy.getOptions(), - //override with any values found in the `rokudeploy.json` file. You can specify current working directory here. - ...util.getOptionsFromJson({ cwd: process.cwd() }) + //load options from the `rokudeploy.json` file. You can specify current working directory here. + ...RokuDeploy.loadOptionsFromJson({ cwd: process.cwd() }) }; await rokuDeploy.sideload(config); ``` @@ -70,10 +68,8 @@ We've removed support for loading `bsconfig.json` files. This was introduced in ```javascript const config = { - //get the default options - ...rokuDeploy.getOptions(), - //override with any values found in config file - ...util.getOptionsFromJson({ configPath: './bsconfig.json' }) + //load options from a custom config file + ...RokuDeploy.loadOptionsFromJson({ configPath: './bsconfig.json' }) }; //call some rokuDeploy function await rokuDeploy.sideload(config); diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index e246dec..8d38e64 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -4473,6 +4473,78 @@ describe('RokuDeploy', () => { }); }); + describe('loadOptionsFromJson', () => { + it('should fill in options from rokudeploy.json', () => { + fsExtra.outputJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); + expect( + RokuDeploy.loadOptionsFromJson({ cwd: rootDir }) + ).to.eql({ + password: 'password' + }); + }); + + it('loads cwd from process', () => { + try { + fsExtra.outputJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); + expect( + RokuDeploy.loadOptionsFromJson() + ).to.eql({ + host: '1.2.3.4' + }); + } finally { + fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); + } + }); + + it('catches invalid json with jsonc parser', () => { + fsExtra.writeJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); + sinon.stub(fsExtra, 'readFileSync').returns(` + { + "rootDir": "src" + `); + let ex; + try { + RokuDeploy.loadOptionsFromJson(); + } catch (e) { + ex = e; + } + expect(ex).to.exist; + expect(ex.message.startsWith('Error parsing')).to.be.true; + fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); + }); + + it('works when loading stagingDir from rokudeploy.json', () => { + sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { + return true; + }); + sinon.stub(fsExtra, 'readFileSync').returns(` + { + "stagingDir": "./staging-dir" + } + `); + let loadedOptions = RokuDeploy.loadOptionsFromJson(); + expect(loadedOptions.stagingDir.endsWith('staging-dir')).to.be.true; + }); + + it('supports jsonc for rokudeploy.json', () => { + fsExtra.writeFileSync(s`${tempDir}/rokudeploy.json`, ` + //leading comment + { + //inner comment + "rootDir": "src" //trailing comment + } + //trailing comment + `); + let loadedOptions = RokuDeploy.loadOptionsFromJson({ cwd: tempDir }); + expect(loadedOptions.rootDir).to.equal('src'); + }); + + it('returns empty object when config file does not exist', () => { + const result = RokuDeploy.loadOptionsFromJson({ cwd: '/nonexistent/path' }); + expect(result).to.eql({}); + }); + }); + describe('generateBaseRequestOptions', () => { it('uses default timeout', () => { const result = rokuDeploy['generateBaseRequestOptions']('test', { host: 'localhost', password: 'test' }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index a5ba5cc..366a952 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -7,9 +7,9 @@ const request = r; import * as JSZip from 'jszip'; import * as errors from './Errors'; import * as xml2js from 'xml2js'; -import { parse as parseJsonc } from 'jsonc-parser'; +import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser'; import { util } from './util'; -import type { FileEntry, RokuDeployConstructorOptions } from './RokuDeployOptions'; +import type { FileEntry, RokuDeployConstructorOptions, RokuDeployOptions } from './RokuDeployOptions'; import { logger } from '@rokucommunity/logger'; import * as dayjs from 'dayjs'; import * as lodash from 'lodash'; @@ -29,6 +29,39 @@ export class RokuDeploy { outFile: 'roku-deploy.zip' }; + /** + * Load options from a rokudeploy.json file. Used by CLI commands to load configuration. + * @param options - Optional cwd and configPath settings + * @returns The parsed options from the config file, or an empty object if not found + */ + public static loadOptionsFromJson(options?: { cwd?: string; configPath?: string }): RokuDeployOptions { + const cwd = options?.cwd ?? process.cwd(); + const configPath = options?.configPath ?? path.join(cwd, 'rokudeploy.json'); + + if (fsExtra.existsSync(configPath)) { + const configFileText = fsExtra.readFileSync(configPath).toString(); + const parseErrors: ParseError[] = []; + const fileOptions = parseJsonc(configFileText, parseErrors, { + allowEmptyContent: true, + allowTrailingComma: true, + disallowComments: false + }); + if (parseErrors.length > 0) { + throw new Error(`Error parsing "${path.resolve(configPath)}": ` + JSON.stringify( + parseErrors.map(x => { + return { + message: printParseErrorCode(x.error), + offset: x.offset, + length: x.length + }; + }) + )); + } + return fileOptions; + } + return {}; + } + /** * Instance-level default options that are merged into every method call */ diff --git a/src/commands/CaptureScreenshotCommand.ts b/src/commands/CaptureScreenshotCommand.ts index ac3967c..3747405 100644 --- a/src/commands/CaptureScreenshotCommand.ts +++ b/src/commands/CaptureScreenshotCommand.ts @@ -1,11 +1,11 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class CaptureScreenshotCommand { async run(args) { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.captureScreenshot(options); diff --git a/src/commands/ConvertToSquashfsCommand.ts b/src/commands/ConvertToSquashfsCommand.ts index 79ebf6e..2f25680 100644 --- a/src/commands/ConvertToSquashfsCommand.ts +++ b/src/commands/ConvertToSquashfsCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class ConvertToSquashfsCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.convertToSquashfs(options); diff --git a/src/commands/CreateSignedPackageCommand.ts b/src/commands/CreateSignedPackageCommand.ts index e2d263d..3bb8ee4 100644 --- a/src/commands/CreateSignedPackageCommand.ts +++ b/src/commands/CreateSignedPackageCommand.ts @@ -1,11 +1,11 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class CreateSignedPackageCommand { async run(args) { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.createSignedPackage(options); diff --git a/src/commands/DeleteDevChannelCommand.ts b/src/commands/DeleteDevChannelCommand.ts index 5bfa269..16070f2 100644 --- a/src/commands/DeleteDevChannelCommand.ts +++ b/src/commands/DeleteDevChannelCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class DeleteDevChannelCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.deleteDevChannel(options); diff --git a/src/commands/GetDevIdCommand.ts b/src/commands/GetDevIdCommand.ts index 710dc92..01aad47 100644 --- a/src/commands/GetDevIdCommand.ts +++ b/src/commands/GetDevIdCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class GetDevIdCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; const devId = await rokuDeploy.getDevId(options); diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 3255fe3..4def00b 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -1,10 +1,9 @@ -import { rokuDeploy } from '../index'; -import { util } from '../util'; +import { rokuDeploy, RokuDeploy, util } from '../index'; export class GetDeviceInfoCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; const outputPath = await rokuDeploy.getDeviceInfo(options); diff --git a/src/commands/KeyDownCommand.ts b/src/commands/KeyDownCommand.ts index ae05b91..0f813d2 100644 --- a/src/commands/KeyDownCommand.ts +++ b/src/commands/KeyDownCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class KeyDownCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.keyDown(options); diff --git a/src/commands/KeyPressCommand.ts b/src/commands/KeyPressCommand.ts index 4554df7..48e03ea 100644 --- a/src/commands/KeyPressCommand.ts +++ b/src/commands/KeyPressCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class KeyPressCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.keyPress(options); diff --git a/src/commands/KeyUpCommand.ts b/src/commands/KeyUpCommand.ts index 36b71ea..2506077 100644 --- a/src/commands/KeyUpCommand.ts +++ b/src/commands/KeyUpCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class KeyUpCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.keyUp(options); diff --git a/src/commands/RekeyDeviceCommand.ts b/src/commands/RekeyDeviceCommand.ts index ae4062a..d6c2878 100644 --- a/src/commands/RekeyDeviceCommand.ts +++ b/src/commands/RekeyDeviceCommand.ts @@ -1,4 +1,4 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy, util } from '../index'; import * as path from 'path'; export class RekeyDeviceCommand { @@ -6,7 +6,7 @@ export class RekeyDeviceCommand { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; if (args.pkg) { diff --git a/src/commands/RemoteControlCommand.ts b/src/commands/RemoteControlCommand.ts index d03bc65..1355fba 100644 --- a/src/commands/RemoteControlCommand.ts +++ b/src/commands/RemoteControlCommand.ts @@ -1,11 +1,11 @@ import * as readline from 'readline'; import type { RokuKey } from '../index'; -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class RemoteControlCommand { run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; diff --git a/src/commands/SendTextCommand.ts b/src/commands/SendTextCommand.ts index 3e11810..3320f49 100644 --- a/src/commands/SendTextCommand.ts +++ b/src/commands/SendTextCommand.ts @@ -1,9 +1,9 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class SendTextCommand { async run(args) { let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.sendText(options); diff --git a/src/commands/SideloadCommand.ts b/src/commands/SideloadCommand.ts index 84d6232..f3894be 100644 --- a/src/commands/SideloadCommand.ts +++ b/src/commands/SideloadCommand.ts @@ -1,11 +1,11 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class SideloadCommand { async run(args) { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; diff --git a/src/commands/StageCommand.ts b/src/commands/StageCommand.ts index 4421715..f8cb099 100644 --- a/src/commands/StageCommand.ts +++ b/src/commands/StageCommand.ts @@ -1,11 +1,11 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class StageCommand { async run(args) { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.stage(options); diff --git a/src/commands/ZipCommand.ts b/src/commands/ZipCommand.ts index 93e978b..80b10e7 100644 --- a/src/commands/ZipCommand.ts +++ b/src/commands/ZipCommand.ts @@ -1,11 +1,11 @@ -import { rokuDeploy, util } from '../index'; +import { rokuDeploy, RokuDeploy } from '../index'; export class ZipCommand { async run(args) { args.cwd ??= process.cwd(); let options = { - ...util.getOptionsFromJson(args), + ...RokuDeploy.loadOptionsFromJson(args), ...args }; await rokuDeploy.zip(options); diff --git a/src/util.spec.ts b/src/util.spec.ts index 1b3f2e7..c6634c4 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -451,73 +451,6 @@ describe('util', () => { }); }); - describe('getOptionsFromJson', () => { - it('should fill in options from rokudeploy.json', () => { - fsExtra.outputJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); - expect( - util.getOptionsFromJson({ cwd: rootDir }) - ).to.eql({ - password: 'password' - }); - }); - - it(`loads cwd from process`, () => { - try { - fsExtra.outputJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); - expect( - util.getOptionsFromJson() - ).to.eql({ - host: '1.2.3.4' - }); - } finally { - fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); - } - }); - - it('catches invalid json with jsonc parser', () => { - fsExtra.writeJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "rootDir": "src" - `); - let ex; - try { - util.getOptionsFromJson(); - } catch (e) { - ex = e; - } - expect(ex).to.exist; - expect(ex.message.startsWith('Error parsing')).to.be.true; - fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); - }); - - it('works when loading stagingDir from rokudeploy.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return true; - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "stagingDir": "./staging-dir" - } - `); - let options = util.getOptionsFromJson(); - expect(options.stagingDir.endsWith('staging-dir')).to.be.true; - }); - - it('supports jsonc for rokudeploy.json', () => { - fsExtra.writeFileSync(s`${tempDir}/rokudeploy.json`, ` - //leading comment - { - //inner comment - "rootDir": "src" //trailing comment - } - //trailing comment - `); - let options = util.getOptionsFromJson({ cwd: tempDir }); - expect(options.rootDir).to.equal('src'); - }); - }); - describe('computeFileDestPath', () => { it('treats {src;dest} without dest as a top-level string', () => { expect( diff --git a/src/util.ts b/src/util.ts index 4442e30..e095320 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,11 +6,10 @@ import * as crypto from 'crypto'; import * as micromatch from 'micromatch'; // eslint-disable-next-line @typescript-eslint/no-require-imports import fastGlob = require('fast-glob'); -import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; +import type { FileEntry } from './RokuDeployOptions'; import type { StandardizedFileEntry } from './RokuDeploy'; import * as isGlob from 'is-glob'; import * as picomatch from 'picomatch'; -import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser'; export class Util { //Map @@ -506,38 +505,6 @@ export class Util { return table.join('\n'); } - - /** - * A function to fill in any missing arguments with JSON values - * Only run when CLI commands are used - */ - public getOptionsFromJson(options?: { cwd?: string; configPath?: string }) { - let fileOptions: RokuDeployOptions = {}; - const cwd = options?.cwd ?? process.cwd(); - const configPath = options?.configPath ?? path.join(cwd, 'rokudeploy.json'); - - if (fsExtra.existsSync(configPath)) { - let configFileText = fsExtra.readFileSync(configPath).toString(); - let parseErrors = [] as ParseError[]; - fileOptions = parseJsonc(configFileText, parseErrors, { - allowEmptyContent: true, - allowTrailingComma: true, - disallowComments: false - }); - if (parseErrors.length > 0) { - throw new Error(`Error parsing "${path.resolve(configPath)}": ` + JSON.stringify( - parseErrors.map(x => { - return { - message: printParseErrorCode(x.error), - offset: x.offset, - length: x.length - }; - }) - )); - } - } - return fileOptions; - } } export let util = new Util();