From c6d25368f075cc29eea37d7282040fea272a4605 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 2 Jan 2024 13:39:51 -0500 Subject: [PATCH 01/63] Make change to assume dest folder. Delete 2 old tests that looked for old behavior, add 2 more tests that look for new behavior --- src/RokuDeploy.spec.ts | 52 ++++++++++++++++++++++++------------------ src/RokuDeploy.ts | 4 +++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index c090fd4..9aaff30 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -3062,28 +3062,6 @@ describe('index', () => { }]); }); - it('works for other globs without dest', async () => { - expect(await getFilePaths([{ - src: `components/screen1/*creen1.brs` - }])).to.eql([{ - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`screen1.brs` - }]); - }); - - it('skips directory folder names for other globs without dest', async () => { - expect(await getFilePaths([{ - //straight wildcard matches folder names too - src: `components/*` - }])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`component1.xml` - }]); - }); - it('applies negated patterns', async () => { writeFiles(rootDir, [ 'components/component1.brs', @@ -3281,6 +3259,36 @@ describe('index', () => { await fsExtra.remove(s`${thisRootDir}/../`); } }); + + it('maintains original file path', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + await rokuDeploy.getFilePaths([ + 'components/CustomButton.brs' + ], rootDir) + ).to.eql([{ + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }]); + }); + + it('correctly assumes file path if not given', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + await rokuDeploy.getFilePaths([ + { src: 'components/*' } + ], rootDir) + ).to.eql([{ + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }, { + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }]); + }); }); describe('computeFileDestPath', () => { diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 37762ce..f3cd8d3 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -329,7 +329,9 @@ export class RokuDeploy { //`pattern` is some other glob magic } else { const fileNameAndExtension = path.basename(srcPath); - result = util.standardizePath(`${entry.dest ?? ''}/${fileNameAndExtension}`); + const foundFilePath = util.standardizePath(`${entry.dest ?? ''}/${fileNameAndExtension}`); + const assumedFilePath = util.stringReplaceInsensitive(srcPath, rootDir, ''); + result = entry.dest ? foundFilePath : assumedFilePath; } result = util.standardizePath( From 2ba8ecae845401c533bb79006373dc7dc32cf8f0 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 2 Jan 2024 14:30:01 -0500 Subject: [PATCH 02/63] Sort files --- src/RokuDeploy.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 9aaff30..76d751a 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -3275,18 +3275,18 @@ describe('index', () => { it('correctly assumes file path if not given', async () => { fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); expect( - await rokuDeploy.getFilePaths([ + (await rokuDeploy.getFilePaths([ { src: 'components/*' } - ], rootDir) + ], rootDir)).sort((a, b) => a.src.localeCompare(b.src)) ).to.eql([{ - src: s`${rootDir}/components/CustomButton.brs`, - dest: s`components/CustomButton.brs` - }, { src: s`${rootDir}/components/component1.brs`, dest: s`components/component1.brs` }, { src: s`${rootDir}/components/component1.xml`, dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` }]); }); }); From cc92e04ca11b2815b4d8b0cb5be8c273b3734e5b Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 16 Jan 2024 10:48:26 -0500 Subject: [PATCH 03/63] Update src/RokuDeploy.ts Co-authored-by: Bronley Plumb --- src/RokuDeploy.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index f3cd8d3..d6d0fc8 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -329,9 +329,11 @@ export class RokuDeploy { //`pattern` is some other glob magic } else { const fileNameAndExtension = path.basename(srcPath); - const foundFilePath = util.standardizePath(`${entry.dest ?? ''}/${fileNameAndExtension}`); - const assumedFilePath = util.stringReplaceInsensitive(srcPath, rootDir, ''); - result = entry.dest ? foundFilePath : assumedFilePath; + if (entry.dest) { + result = util.standardizePath(`${entry.dest ?? ''}/${fileNameAndExtension}`); + } else { + result = util.stringReplaceInsensitive(srcPath, rootDir, ''); + } } result = util.standardizePath( From 9bbd579ffa13777f8f36ecba0d80b7c040a2049f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 17 Jan 2024 13:30:18 -0500 Subject: [PATCH 04/63] Removed redundant entry.dest is null situation --- src/RokuDeploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index d6d0fc8..e7a0462 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -330,7 +330,7 @@ export class RokuDeploy { } else { const fileNameAndExtension = path.basename(srcPath); if (entry.dest) { - result = util.standardizePath(`${entry.dest ?? ''}/${fileNameAndExtension}`); + result = util.standardizePath(`${entry.dest}/${fileNameAndExtension}`); } else { result = util.stringReplaceInsensitive(srcPath, rootDir, ''); } From 460cbf673b37f344825a34dc9d70f8f9eae57a9f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Fri, 19 Jan 2024 15:02:47 -0500 Subject: [PATCH 05/63] Added a few commands, not all are working correctly --- src/RokuDeploy.ts | 2 +- src/cli.ts | 173 ++++++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 2 + tsconfig.json | 2 +- 4 files changed, 172 insertions(+), 7 deletions(-) mode change 100644 => 100755 src/cli.ts diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index f3cd8d3..c7ed29d 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -957,7 +957,7 @@ export class RokuDeploy { * Centralizes getting output zip file path based on passed in options * @param options */ - public getOutputZipFilePath(options: GetOutputZipFilePathOptions) { + public getOutputZipFilePath(options?: GetOutputZipFilePathOptions) { options = this.getOptions(options) as any; let zipFileName = options.outFile; diff --git a/src/cli.ts b/src/cli.ts old mode 100644 new mode 100755 index 063f5fd..5675926 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,170 @@ #!/usr/bin/env node -import { deploy } from './index'; -deploy().then((...args) => { - console.log(...args); -}, (...args) => { - console.error(...args); +import * as yargs from 'yargs'; +import { stagingDir } from './testUtils.spec'; +import { prepublishToStaging, zipPackage, createPackage, publish, getOutputZipFilePath, getOutputPkgFilePath, getDeviceInfo, getDevId, zipFolder } from './index'; +const outDir = './out'; + +new Promise((resolve, reject) => { + // TODO: is this necessary?vv + // eslint-disable-next-line + yargs + .command('prepublishToStaging', 'Copies all of the referenced files to the staging folder', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: true }) + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: true }); + }, (args: any) => { + console.log('prepublishToStaging'); + prepublishToStaging({ + files: [ + 'manifest' + ], + stagingDir: args.stagingDir, + rootDir: args.rootDir + }).then(() => { + console.error('SUCCESS'); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: Should we have defaults for these^^ + // TODO: This doesn't work + }) + + .command('zipPackage', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }); + }, (args: any) => { + console.log('zipPackage'); + zipPackage({ + stagingDir: stagingDir, + outDir: args.outDir + }).then(() => { + console.error('SUCCESS'); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: Missing manifest file + }) + + .command('createPackage', 'Create a zip folder containing all of the specified roku project files', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }); + }, (args: any) => { + console.log('createPackage'); + createPackage({ + files: [ + 'manifest' + ], + stagingDir: '.tmp/dist', + outDir: args.outDir, + rootDir: './src' + }).then(() => { + console.error('SUCCESS'); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: Missing manifest file + }) + + .command('publish', 'Publish a pre-existing packaged zip file to a remote Roku', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: true }) + .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', default: 'roku-deploy', demandOption: false }); + }, (args: any) => { + console.log('publish'); + publish({ + host: args.host, + password: args.password, + outDir: args.outDir, + outFile: args.outFile + }).then(() => { + console.error('SUCCESS'); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: Times out + }) + + // TODO: + // convertToSquashfs + // rekeyDevice + // signExistingPackage + // retrieveSignedPackage + // deploy + // deleteInstalledChannel + // takeScreenshot + // deployAndSignPackage - TODO: does the same thing as deploy but also signs package...is it necessary? + + .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { + // EXAMPLE: npx roku-deploy getOutputZipFilePath + return builder; + }, (args: any) => { + console.log('getOutputZipFilePath'); + console.log(getOutputZipFilePath({})); + }) + + .command('getOutputPkgFilePath', 'Centralizes getting output pkg file path based on passed in options', (builder) => { + // EXAMPLE: npx roku-deploy getOutputPkgFilePath + return builder; + }, (args: any) => { + console.log('getOutputPkgFilePath'); + let result = getOutputPkgFilePath({}); + console.log(result); + }) + + .command('getDeviceInfo', 'Get the `device-info` response from a Roku device', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }); + }, (args: any) => { + console.log('getDeviceInfo'); + let result = getDeviceInfo({ + host: args.host + }).then(() => { + console.error('SUCCESS', result); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: returns pending promise? + }) + + .command('getDevId', 'Get Dev ID', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }); + }, (args: any) => { + console.log('getDevId'); + let result = getDevId({ + host: args.host + }).then(() => { + console.error('SUCCESS', result); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + // TODO: returns pending promise? + }) + + .command('zipFolder', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { + // EXAMPLE: npx roku-deploy zipFolder --srcFolder ./src --zipFilePath ./output.zip + return builder + .option('srcFolder', { type: 'string', description: 'The folder that should be zipped', demandOption: true }) + .option('zipFilePath', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: true }); + }, (args: any) => { + console.log('zipFolder'); + zipFolder( + args.srcFolder, + args.zipFilePath + ).then(() => { + console.error('SUCCESS'); + }, (error) => { + console.error('ERROR', error, '\n', args); + }); + }) + + .argv; +}).catch((e) => { + console.error(e); + process.exit(1); }); diff --git a/src/index.ts b/src/index.ts index dff33ac..d68b4d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ let retrieveSignedPackage = RokuDeploy.prototype.retrieveSignedPackage.bind(roku let signExistingPackage = RokuDeploy.prototype.signExistingPackage.bind(rokuDeploy); let stringifyManifest = RokuDeploy.prototype.stringifyManifest.bind(rokuDeploy); let takeScreenshot = RokuDeploy.prototype.takeScreenshot.bind(rokuDeploy); +let getDevId = RokuDeploy.prototype.getDevId.bind(rokuDeploy); let zipFolder = RokuDeploy.prototype.zipFolder.bind(rokuDeploy); let zipPackage = RokuDeploy.prototype.zipPackage.bind(rokuDeploy); @@ -56,6 +57,7 @@ export { signExistingPackage, stringifyManifest, takeScreenshot, + getDevId, zipFolder, zipPackage }; diff --git a/tsconfig.json b/tsconfig.json index 381ba26..5d1cd50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "include": [ "src/**/*.ts", "device.spec.ts" - ], +, "src/cli.ts" ], "ts-node": { "transpileOnly": true } From 6db962aceaf19ea48d46a7f12d4478bdb170c48a Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 31 Jan 2024 13:37:39 -0500 Subject: [PATCH 06/63] Move commands to their own file --- src/commands/ConvertToSquashfsCommand.ts | 10 ++++++++++ src/commands/CreatePackageCommand.ts | 11 +++++++++++ src/commands/DeleteInstalledChannelCommand.ts | 10 ++++++++++ src/commands/DeployCommand.ts | 11 +++++++++++ src/commands/PrepublishCommand.ts | 10 ++++++++++ src/commands/PublishCommand.ts | 12 ++++++++++++ src/commands/RekeyDeviceCommand.ts | 14 ++++++++++++++ src/commands/RetrieveSignedPackageCommand.ts | 11 +++++++++++ src/commands/SignExistingPackageCommand.ts | 12 ++++++++++++ src/commands/TakeScreenshotCommand.ts | 10 ++++++++++ src/commands/ZipPackageCommand.ts | 10 ++++++++++ 11 files changed, 121 insertions(+) create mode 100644 src/commands/ConvertToSquashfsCommand.ts create mode 100644 src/commands/CreatePackageCommand.ts create mode 100644 src/commands/DeleteInstalledChannelCommand.ts create mode 100644 src/commands/DeployCommand.ts create mode 100644 src/commands/PrepublishCommand.ts create mode 100644 src/commands/PublishCommand.ts create mode 100644 src/commands/RekeyDeviceCommand.ts create mode 100644 src/commands/RetrieveSignedPackageCommand.ts create mode 100644 src/commands/SignExistingPackageCommand.ts create mode 100644 src/commands/TakeScreenshotCommand.ts create mode 100644 src/commands/ZipPackageCommand.ts diff --git a/src/commands/ConvertToSquashfsCommand.ts b/src/commands/ConvertToSquashfsCommand.ts new file mode 100644 index 0000000..b1e1e7b --- /dev/null +++ b/src/commands/ConvertToSquashfsCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class ConvertToSquashfsCommand { + async run(args) { + await rokuDeploy.convertToSquashfs({ + host: args.host, + password: args.password + }); + } +} diff --git a/src/commands/CreatePackageCommand.ts b/src/commands/CreatePackageCommand.ts new file mode 100644 index 0000000..f4b4718 --- /dev/null +++ b/src/commands/CreatePackageCommand.ts @@ -0,0 +1,11 @@ +import { rokuDeploy } from '../index'; + +export class CreatePackageCommand { + async run(args) { + await rokuDeploy.createPackage({ + stagingDir: args.stagingDir, + outDir: args.outDir, + rootDir: args.rootDir + }); + } +} diff --git a/src/commands/DeleteInstalledChannelCommand.ts b/src/commands/DeleteInstalledChannelCommand.ts new file mode 100644 index 0000000..1ec9c29 --- /dev/null +++ b/src/commands/DeleteInstalledChannelCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class DeleteInstalledChannelCommand { + async run(args) { + await rokuDeploy.deleteInstalledChannel({ + host: args.host, + password: args.password + }); + } +} diff --git a/src/commands/DeployCommand.ts b/src/commands/DeployCommand.ts new file mode 100644 index 0000000..5947deb --- /dev/null +++ b/src/commands/DeployCommand.ts @@ -0,0 +1,11 @@ +import { rokuDeploy } from '../index'; + +export class DeployCommand { + async run(args) { + await rokuDeploy.deploy({ + host: args.host, + password: args.password, + rootDir: args.rootDir + }); + } +} diff --git a/src/commands/PrepublishCommand.ts b/src/commands/PrepublishCommand.ts new file mode 100644 index 0000000..2ca65cc --- /dev/null +++ b/src/commands/PrepublishCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class PrepublishCommand { + async run(args) { + await rokuDeploy.prepublishToStaging({ + stagingDir: args.stagingDir, + rootDir: args.rootDir + }); + } +} diff --git a/src/commands/PublishCommand.ts b/src/commands/PublishCommand.ts new file mode 100644 index 0000000..91554fa --- /dev/null +++ b/src/commands/PublishCommand.ts @@ -0,0 +1,12 @@ +import { rokuDeploy } from '../index'; + +export class PublishCommand { + async run(args) { + await rokuDeploy.publish({ + host: args.host, + password: args.password, + outDir: args.outDir, + outFile: args.outFile + }); + } +} \ No newline at end of file diff --git a/src/commands/RekeyDeviceCommand.ts b/src/commands/RekeyDeviceCommand.ts new file mode 100644 index 0000000..7098772 --- /dev/null +++ b/src/commands/RekeyDeviceCommand.ts @@ -0,0 +1,14 @@ +import { rokuDeploy } from '../index'; + +export class RekeyDeviceCommand { + async run(args) { + await rokuDeploy.rekeyDevice({ + host: args.host, + password: args.password, + rekeySignedPackage: args.rekeySignedPackage, + signingPassword: args.signingPassword, + rootDir: args.rootDir, + devId: args.devId + }); + } +} diff --git a/src/commands/RetrieveSignedPackageCommand.ts b/src/commands/RetrieveSignedPackageCommand.ts new file mode 100644 index 0000000..cfecba6 --- /dev/null +++ b/src/commands/RetrieveSignedPackageCommand.ts @@ -0,0 +1,11 @@ +import { rokuDeploy } from '../index'; + +export class RetrieveSignedPackageCommand { + async run(args) { + await rokuDeploy.retrieveSignedPackage('path_to_pkg', { + host: args.host, + password: args.password, + outFile: args.outFile + }); + } +} diff --git a/src/commands/SignExistingPackageCommand.ts b/src/commands/SignExistingPackageCommand.ts new file mode 100644 index 0000000..2d5733f --- /dev/null +++ b/src/commands/SignExistingPackageCommand.ts @@ -0,0 +1,12 @@ +import { rokuDeploy } from '../index'; + +export class SignExistingPackageCommand { + async run(args) { + await rokuDeploy.signExistingPackage({ + host: args.host, + password: args.password, + signingPassword: args.signingPassword, + stagingDir: args.stagingDir + }); + } +} diff --git a/src/commands/TakeScreenshotCommand.ts b/src/commands/TakeScreenshotCommand.ts new file mode 100644 index 0000000..ac5b7bc --- /dev/null +++ b/src/commands/TakeScreenshotCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class TakeScreenshotCommand { + async run(args) { + await rokuDeploy.takeScreenshot({ + host: args.host, + password: args.password + }); + } +} diff --git a/src/commands/ZipPackageCommand.ts b/src/commands/ZipPackageCommand.ts new file mode 100644 index 0000000..7ef1f09 --- /dev/null +++ b/src/commands/ZipPackageCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class ZipPackageCommand { + async run(args) { + await rokuDeploy.zipPackage({ + stagingDir: args.stagingDir, + outDir: args.outDir + }); + } +} From 20eb21f133ea9f5f307a08efa089340f6c93b14b Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 1 Feb 2024 00:30:59 -0500 Subject: [PATCH 07/63] More command files --- src/commands/GetDevIdCommand.ts | 9 +++++++++ src/commands/GetDeviceInfoCommand.ts | 9 +++++++++ src/commands/GetOutputPkgFilePathCommand.ts | 10 ++++++++++ src/commands/GetOutputZipFilePathCommand.ts | 10 ++++++++++ src/commands/ZipFolderCommand.ts | 10 ++++++++++ 5 files changed, 48 insertions(+) create mode 100644 src/commands/GetDevIdCommand.ts create mode 100644 src/commands/GetDeviceInfoCommand.ts create mode 100644 src/commands/GetOutputPkgFilePathCommand.ts create mode 100644 src/commands/GetOutputZipFilePathCommand.ts create mode 100644 src/commands/ZipFolderCommand.ts diff --git a/src/commands/GetDevIdCommand.ts b/src/commands/GetDevIdCommand.ts new file mode 100644 index 0000000..72ccde8 --- /dev/null +++ b/src/commands/GetDevIdCommand.ts @@ -0,0 +1,9 @@ +import { rokuDeploy } from '../index'; + +export class GetDevIdCommand { + async run(args) { + await rokuDeploy.getDevId({ + host: args.host + }); + } +} diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts new file mode 100644 index 0000000..42e6580 --- /dev/null +++ b/src/commands/GetDeviceInfoCommand.ts @@ -0,0 +1,9 @@ +import { rokuDeploy } from '../index'; + +export class GetDeviceInfoCommand { + async run(args) { + await rokuDeploy.getDeviceInfo({ + host: args.host + }); + } +} diff --git a/src/commands/GetOutputPkgFilePathCommand.ts b/src/commands/GetOutputPkgFilePathCommand.ts new file mode 100644 index 0000000..cfa9450 --- /dev/null +++ b/src/commands/GetOutputPkgFilePathCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class GetOutputPkgFilePathCommand { + run(args) { + rokuDeploy.getOutputPkgFilePath({ + outFile: args.outFile, + outDir: args.outDir + }); + } +} diff --git a/src/commands/GetOutputZipFilePathCommand.ts b/src/commands/GetOutputZipFilePathCommand.ts new file mode 100644 index 0000000..a8f697f --- /dev/null +++ b/src/commands/GetOutputZipFilePathCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class GetOutputZipFilePathCommand { + run(args) { + rokuDeploy.getOutputZipFilePath({ + outFile: args.outFile, + outDir: args.outDir + }); + } +} diff --git a/src/commands/ZipFolderCommand.ts b/src/commands/ZipFolderCommand.ts new file mode 100644 index 0000000..edf9078 --- /dev/null +++ b/src/commands/ZipFolderCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class ZipFolderCommand { + async run(args) { + await rokuDeploy.zipFolder( + args.srcFolder, + args.zipFilePath + ); + } +} From 4e4b66a9a5ec1977c06e1fe85df7698650b4d8aa Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 1 Feb 2024 18:59:39 -0500 Subject: [PATCH 08/63] Change commands to help with tests --- src/commands/GetDeviceInfoCommand.ts | 3 ++- src/commands/GetOutputPkgFilePathCommand.ts | 3 ++- src/commands/GetOutputZipFilePathCommand.ts | 3 ++- src/commands/RetrieveSignedPackageCommand.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 42e6580..0670849 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -2,8 +2,9 @@ import { rokuDeploy } from '../index'; export class GetDeviceInfoCommand { async run(args) { - await rokuDeploy.getDeviceInfo({ + const outputPath = await rokuDeploy.getDeviceInfo({ host: args.host }); + console.log(JSON.stringify(outputPath)); } } diff --git a/src/commands/GetOutputPkgFilePathCommand.ts b/src/commands/GetOutputPkgFilePathCommand.ts index cfa9450..78fff13 100644 --- a/src/commands/GetOutputPkgFilePathCommand.ts +++ b/src/commands/GetOutputPkgFilePathCommand.ts @@ -2,9 +2,10 @@ import { rokuDeploy } from '../index'; export class GetOutputPkgFilePathCommand { run(args) { - rokuDeploy.getOutputPkgFilePath({ + const outputPath = rokuDeploy.getOutputPkgFilePath({ outFile: args.outFile, outDir: args.outDir }); + console.log(outputPath); } } diff --git a/src/commands/GetOutputZipFilePathCommand.ts b/src/commands/GetOutputZipFilePathCommand.ts index a8f697f..5525057 100644 --- a/src/commands/GetOutputZipFilePathCommand.ts +++ b/src/commands/GetOutputZipFilePathCommand.ts @@ -2,9 +2,10 @@ import { rokuDeploy } from '../index'; export class GetOutputZipFilePathCommand { run(args) { - rokuDeploy.getOutputZipFilePath({ + const outputPath = rokuDeploy.getOutputZipFilePath({ outFile: args.outFile, outDir: args.outDir }); + console.log(outputPath); } } diff --git a/src/commands/RetrieveSignedPackageCommand.ts b/src/commands/RetrieveSignedPackageCommand.ts index cfecba6..411dfd9 100644 --- a/src/commands/RetrieveSignedPackageCommand.ts +++ b/src/commands/RetrieveSignedPackageCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class RetrieveSignedPackageCommand { async run(args) { - await rokuDeploy.retrieveSignedPackage('path_to_pkg', { + await rokuDeploy.retrieveSignedPackage(args.pathToPkg, { host: args.host, password: args.password, outFile: args.outFile From 2b809e6886ad0c720eb9a2f81de63a1ff67b87d2 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 1 Feb 2024 19:00:49 -0500 Subject: [PATCH 09/63] Add testing suite for all cli (2 tests are not working) --- src/cli.spec.ts | 324 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/cli.spec.ts diff --git a/src/cli.spec.ts b/src/cli.spec.ts new file mode 100644 index 0000000..7bddc32 --- /dev/null +++ b/src/cli.spec.ts @@ -0,0 +1,324 @@ +import * as childProcess from 'child_process'; +import { cwd, expectPathExists, rootDir, stagingDir, tempDir, outDir } from './testUtils.spec'; +import * as fsExtra from 'fs-extra'; +import { expect } from 'chai'; +import * as path from 'path'; +import { createSandbox } from 'sinon'; +import { rokuDeploy } from './index'; +import { PublishCommand } from './commands/PublishCommand'; +import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; +import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; +import { SignExistingPackageCommand } from './commands/SignExistingPackageCommand'; +import { DeployCommand } from './commands/DeployCommand'; +import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; +import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; +import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; +import { GetDevIdCommand } from './commands/GetDevIdCommand'; +import { RetrieveSignedPackageCommand } from './commands/RetrieveSignedPackageCommand'; + +const sinon = createSandbox(); + +function execSync(command: string) { + const output = childProcess.execSync(command, { cwd: tempDir }); + process.stdout.write(output); + return output; + // return childProcess.execSync(command, { stdio: 'inherit', cwd: tempDir }); +} +describe('cli', () => { + before(function build() { + this.timeout(20000); + execSync('npm run build'); + }); + beforeEach(() => { + fsExtra.emptyDirSync(tempDir); + //most tests depend on a manifest file existing, so write an empty one + fsExtra.outputFileSync(`${rootDir}/manifest`, ''); + }); + afterEach(() => { + fsExtra.removeSync(tempDir); + }); + + it('Successfully runs prepublishToStaging', () => { + //make the files + // fsExtra.outputFileSync(`${rootDir}/manifest`, ''); + fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); + + expect(() => { + execSync(`node ${cwd}/dist/cli.js prepublishToStaging --stagingDir ${stagingDir} --rootDir ${rootDir}`); + }).to.not.throw(); + }); + + it('Successfully copies rootDir folder to staging folder', () => { + fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); + + execSync(`node ${cwd}/dist/cli.js prepublishToStaging --rootDir ${rootDir} --stagingDir ${stagingDir}`); + + expectPathExists(`${stagingDir}/source/main.brs`); + }); + + it('Successfully uses zipPackage to create .zip', () => { + fsExtra.outputFileSync(`${stagingDir}/manifest`, ''); + + execSync(`node ${cwd}/dist/cli.js zipPackage --stagingDir ${stagingDir} --outDir ${outDir}`); + expectPathExists(`${outDir}/roku-deploy.zip`); + }); + + it('Successfully uses createPackage to create .pkg', () => { + execSync(`node ${cwd}/dist/cli.js createPackage --stagingDir ${stagingDir} --rootDir ${rootDir} --outDir ${outDir}`); + expectPathExists(`${outDir}/roku-deploy.zip`); + }); + + it('Publish passes proper options', async () => { + const stub = sinon.stub(rokuDeploy, 'publish').callsFake(async () => { + return Promise.resolve({ + message: 'Publish successful', + results: {} + }); + }); + + const command = new PublishCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536', + outDir: outDir, + outFile: 'rokudeploy-outfile' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536', + outDir: outDir, + outFile: 'rokudeploy-outfile' + }); + }); + + it('Converts to squashfs', async () => { + const stub = sinon.stub(rokuDeploy, 'convertToSquashfs').callsFake(async () => { + return Promise.resolve(); + }); + + const command = new ConvertToSquashfsCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536' + }); + }); + + it('Rekeys a device', async () => { + const stub = sinon.stub(rokuDeploy, 'rekeyDevice').callsFake(async () => { + return Promise.resolve(); + }); + + const command = new RekeyDeviceCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536', + rekeySignedPackage: `${tempDir}/testSignedPackage.pkg`, + signingPassword: '12345', + rootDir: rootDir, + devId: 'abcde' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536', + rekeySignedPackage: `${tempDir}/testSignedPackage.pkg`, + signingPassword: '12345', + rootDir: rootDir, + devId: 'abcde' + }); + }); + + it('Signs an existing package', async () => { + const stub = sinon.stub(rokuDeploy, 'signExistingPackage').callsFake(async () => { + return Promise.resolve(''); + }); + + const command = new SignExistingPackageCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536', + signingPassword: undefined, + stagingDir: stagingDir + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536', + signingPassword: undefined, + stagingDir: stagingDir + }); + }); + + it('Retrieves a signed package', async () => { + const stub = sinon.stub(rokuDeploy, 'retrieveSignedPackage').callsFake(async () => { + return Promise.resolve(''); + }); + + const command = new RetrieveSignedPackageCommand(); + await command.run({ + pathToPkg: 'path_to_pkg', + host: '1.2.3.4', + password: '5536', + outFile: 'roku-deploy-test' + }); + console.log(stub.getCall(0).args[0]); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + pathToPkg: 'path_to_pkg', + host: '1.2.3.4', + password: '5536', + outFile: 'roku-deploy-test' + });//TODO: fix! + }); + + it('Deploys a package', async () => { + const stub = sinon.stub(rokuDeploy, 'deploy').callsFake(async () => { + return Promise.resolve({ + message: 'Convert successful', + results: {} + }); + }); + + const command = new DeployCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536', + rootDir: rootDir + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536', + rootDir: rootDir + }); + }); + + it('Deletes an installed channel', async () => { + const stub = sinon.stub(rokuDeploy, 'deleteInstalledChannel').callsFake(async () => { + return Promise.resolve({ response: {}, body: {} }); + }); + + const command = new DeleteInstalledChannelCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536' + }); + }); + + it('Takes a screenshot', async () => { + const stub = sinon.stub(rokuDeploy, 'takeScreenshot').callsFake(async () => { + return Promise.resolve(''); + }); + + const command = new TakeScreenshotCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4', + password: '5536' + }); + }); + + it('Gets output zip file path', () => { + let zipFilePath = execSync(`node ${cwd}/dist/cli.js getOutputZipFilePath --outFile "roku-deploy" --outDir ${outDir}`).toString(); + + expect(zipFilePath.trim()).to.equal(path.join(path.resolve(outDir), 'roku-deploy.zip')); + }); + + it('Gets output pkg file path', () => { + let pkgFilePath = execSync(`node ${cwd}/dist/cli.js getOutputPkgFilePath --outFile "roku-deploy" --outDir ${outDir}`).toString(); + + expect(pkgFilePath.trim()).to.equal(path.join(path.resolve(outDir), 'roku-deploy.pkg')); + }); + + it('Device info arguments are correct', async () => { + const stub = sinon.stub(rokuDeploy, 'getDeviceInfo').callsFake(async () => { + return Promise.resolve({ + response: {}, + body: {} + }); + }); + + const command = new GetDeviceInfoCommand(); + await command.run({ + host: '1.2.3.4' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4' + }); + }); + + it('Prints device info to console', async () => { + let consoleOutput = ''; + sinon.stub(console, 'log').callsFake((...args) => { + consoleOutput += args.join(' ') + '\n'; //TODO: I don't think this is accurately adding a new line + }); + sinon.stub(rokuDeploy, 'getDeviceInfo').returns(Promise.resolve({ + 'device-id': '1234', + 'serial-number': 'abcd' + })); + await new GetDeviceInfoCommand().run({ + host: '1.2.3.4' + }); + expect(consoleOutput.trim()).to.eql( + '{"device-id":"1234","serial-number":"abcd"}' + ); + }); //TODO: This passes when it is it.only, but fails in the larger test suite? + + it('Gets dev id', async () => { + const stub = sinon.stub(rokuDeploy, 'getDevId').callsFake(async () => { + return Promise.resolve(''); + }); + + const command = new GetDevIdCommand(); + await command.run({ + host: '1.2.3.4', + password: '5536' + }); + + expect( + stub.getCall(0).args[0] + ).to.eql({ + host: '1.2.3.4' + }); + }); + + it('Zips a folder', () => { + execSync(`node ${cwd}/dist/cli.js zipFolder --srcFolder ${rootDir} --zipFilePath "roku-deploy.zip"`); + + expectPathExists(`${tempDir}/roku-deploy.zip`); + }); +}); From 13f2ca4f6c011b51569d45158cf4ceb50b43fefb Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 1 Feb 2024 19:02:02 -0500 Subject: [PATCH 10/63] Updated cli file after adding in commands and tests --- src/cli.ts | 326 ++++++++++++++++++++++++++--------------------------- 1 file changed, 158 insertions(+), 168 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 5675926..002551f 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,170 +1,160 @@ #!/usr/bin/env node import * as yargs from 'yargs'; -import { stagingDir } from './testUtils.spec'; -import { prepublishToStaging, zipPackage, createPackage, publish, getOutputZipFilePath, getOutputPkgFilePath, getDeviceInfo, getDevId, zipFolder } from './index'; -const outDir = './out'; - -new Promise((resolve, reject) => { - // TODO: is this necessary?vv - // eslint-disable-next-line - yargs - .command('prepublishToStaging', 'Copies all of the referenced files to the staging folder', (builder) => { - return builder - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: true }) - .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: true }); - }, (args: any) => { - console.log('prepublishToStaging'); - prepublishToStaging({ - files: [ - 'manifest' - ], - stagingDir: args.stagingDir, - rootDir: args.rootDir - }).then(() => { - console.error('SUCCESS'); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: Should we have defaults for these^^ - // TODO: This doesn't work - }) - - .command('zipPackage', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { - return builder - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }); - }, (args: any) => { - console.log('zipPackage'); - zipPackage({ - stagingDir: stagingDir, - outDir: args.outDir - }).then(() => { - console.error('SUCCESS'); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: Missing manifest file - }) - - .command('createPackage', 'Create a zip folder containing all of the specified roku project files', (builder) => { - return builder - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) - .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }); - }, (args: any) => { - console.log('createPackage'); - createPackage({ - files: [ - 'manifest' - ], - stagingDir: '.tmp/dist', - outDir: args.outDir, - rootDir: './src' - }).then(() => { - console.error('SUCCESS'); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: Missing manifest file - }) - - .command('publish', 'Publish a pre-existing packaged zip file to a remote Roku', (builder) => { - return builder - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }) - .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: true }) - .option('outDir', { type: 'string', description: 'The output directory', default: outDir, demandOption: false }) - .option('outFile', { type: 'string', description: 'The output file', default: 'roku-deploy', demandOption: false }); - }, (args: any) => { - console.log('publish'); - publish({ - host: args.host, - password: args.password, - outDir: args.outDir, - outFile: args.outFile - }).then(() => { - console.error('SUCCESS'); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: Times out - }) - - // TODO: - // convertToSquashfs - // rekeyDevice - // signExistingPackage - // retrieveSignedPackage - // deploy - // deleteInstalledChannel - // takeScreenshot - // deployAndSignPackage - TODO: does the same thing as deploy but also signs package...is it necessary? - - .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { - // EXAMPLE: npx roku-deploy getOutputZipFilePath - return builder; - }, (args: any) => { - console.log('getOutputZipFilePath'); - console.log(getOutputZipFilePath({})); - }) - - .command('getOutputPkgFilePath', 'Centralizes getting output pkg file path based on passed in options', (builder) => { - // EXAMPLE: npx roku-deploy getOutputPkgFilePath - return builder; - }, (args: any) => { - console.log('getOutputPkgFilePath'); - let result = getOutputPkgFilePath({}); - console.log(result); - }) - - .command('getDeviceInfo', 'Get the `device-info` response from a Roku device', (builder) => { - return builder - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }); - }, (args: any) => { - console.log('getDeviceInfo'); - let result = getDeviceInfo({ - host: args.host - }).then(() => { - console.error('SUCCESS', result); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: returns pending promise? - }) - - .command('getDevId', 'Get Dev ID', (builder) => { - return builder - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: true }); - }, (args: any) => { - console.log('getDevId'); - let result = getDevId({ - host: args.host - }).then(() => { - console.error('SUCCESS', result); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - // TODO: returns pending promise? - }) - - .command('zipFolder', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { - // EXAMPLE: npx roku-deploy zipFolder --srcFolder ./src --zipFilePath ./output.zip - return builder - .option('srcFolder', { type: 'string', description: 'The folder that should be zipped', demandOption: true }) - .option('zipFilePath', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: true }); - }, (args: any) => { - console.log('zipFolder'); - zipFolder( - args.srcFolder, - args.zipFilePath - ).then(() => { - console.error('SUCCESS'); - }, (error) => { - console.error('ERROR', error, '\n', args); - }); - }) - - .argv; -}).catch((e) => { - console.error(e); - process.exit(1); -}); +import { PrepublishCommand } from './commands/PrepublishCommand'; +import { ZipPackageCommand } from './commands/ZipPackageCommand'; +import { CreatePackageCommand } from './commands/CreatePackageCommand'; +import { PublishCommand } from './commands/PublishCommand'; +import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; +import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; +import { SignExistingPackageCommand } from './commands/SignExistingPackageCommand'; +import { RetrieveSignedPackageCommand } from './commands/RetrieveSignedPackageCommand'; +import { DeployCommand } from './commands/DeployCommand'; +import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; +import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; +import { GetOutputZipFilePathCommand } from './commands/GetOutputZipFilePathCommand'; +import { GetOutputPkgFilePathCommand } from './commands/GetOutputPkgFilePathCommand'; +import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; +import { GetDevIdCommand } from './commands/GetDevIdCommand'; +import { ZipFolderCommand } from './commands/ZipFolderCommand'; + +void yargs + .command('prepublishToStaging', 'Copies all of the referenced files to the staging folder', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }); + }, (args: any) => { + return new PrepublishCommand().run(args); + }) + + .command('zipPackage', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); + }, (args: any) => { + return new ZipPackageCommand().run(args); + }) + + .command('createPackage', 'Create a zip folder containing all of the specified roku project files', (builder) => { + return builder + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); + }, (args: any) => { + return new CreatePackageCommand().run(args); + }) + + .command('publish', 'Publish a pre-existing packaged zip file to a remote Roku', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); + }, (args: any) => { + return new PublishCommand().run(args); + }) + + .command('convertToSquashfs', 'Convert a pre-existing packaged zip file to a squashfs file', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); + }, (args: any) => { + return new ConvertToSquashfsCommand().run(args); + }) + + .command('rekeyDevice', 'Rekey a device', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('rekeySignedPackage', { type: 'string', description: 'The signed package to be used for rekeying', demandOption: false }) + .option('signingPassword', { type: 'string', description: 'The password of the signing key', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }) + .option('devId', { type: 'string', description: 'The dev ID', demandOption: false }); + }, (args: any) => { + return new RekeyDeviceCommand().run(args); + }) + + .command('signExistingPackage', 'Sign a package', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('signingPassword', { type: 'string', description: 'The password of the signing key', demandOption: false }) + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }); + }, (args: any) => { + return new SignExistingPackageCommand().run(args); + }) + + .command('retrieveSignedPackage', 'Retrieve a signed package', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); + }, (args: any) => { + return new RetrieveSignedPackageCommand().run(args); + }) + + .command('deploy', 'Deploy a pre-existing packaged zip file to a remote Roku', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }); + }, (args: any) => { + return new DeployCommand().run(args); + }) + + .command('deleteInstalledChannel', 'Delete an installed channel', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); + }, (args: any) => { + return new DeleteInstalledChannelCommand().run(args); + }) + + .command('takeScreenshot', 'Take a screenshot', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); + }, (args: any) => { + return new TakeScreenshotCommand().run(args); + }) + + .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { + return builder + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); + return builder; + }, (args: any) => { + return new GetOutputZipFilePathCommand().run(args); + }) + + .command('getOutputPkgFilePath', 'Centralizes getting output pkg file path based on passed in options', (builder) => { + return builder + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); + }, (args: any) => { + return new GetOutputPkgFilePathCommand().run(args); + }) + + .command('getDeviceInfo', 'Get the `device-info` response from a Roku device', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }); + }, (args: any) => { + return new GetDeviceInfoCommand().run(args); + }) + + .command('getDevId', 'Get Dev ID', (builder) => { + return builder + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }); + }, (args: any) => { + return new GetDevIdCommand().run(args); + }) + + .command('zipFolder', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { + return builder + .option('srcFolder', { type: 'string', description: 'The folder that should be zipped', demandOption: false }) + .option('zipFilePath', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: false }); + }, (args: any) => { + console.log('args', args); + return new ZipFolderCommand().run(args); + }) + + .argv; From 2d134412f4a7a8119a383f8b5990e41c7dae6159 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Fri, 2 Feb 2024 12:05:09 -0500 Subject: [PATCH 11/63] Fixed a few test cases --- src/cli.spec.ts | 31 +++++++++++++++++----------- src/commands/GetDeviceInfoCommand.ts | 4 ++-- src/util.ts | 14 +++++++++++++ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 7bddc32..4cf25f3 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -22,7 +22,6 @@ function execSync(command: string) { const output = childProcess.execSync(command, { cwd: tempDir }); process.stdout.write(output); return output; - // return childProcess.execSync(command, { stdio: 'inherit', cwd: tempDir }); } describe('cli', () => { before(function build() { @@ -33,14 +32,15 @@ describe('cli', () => { fsExtra.emptyDirSync(tempDir); //most tests depend on a manifest file existing, so write an empty one fsExtra.outputFileSync(`${rootDir}/manifest`, ''); + sinon.restore(); }); afterEach(() => { fsExtra.removeSync(tempDir); + sinon.restore(); }); it('Successfully runs prepublishToStaging', () => { //make the files - // fsExtra.outputFileSync(`${rootDir}/manifest`, ''); fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); expect(() => { @@ -175,16 +175,14 @@ describe('cli', () => { password: '5536', outFile: 'roku-deploy-test' }); - console.log(stub.getCall(0).args[0]); expect( - stub.getCall(0).args[0] - ).to.eql({ - pathToPkg: 'path_to_pkg', + stub.getCall(0).args + ).to.eql(['path_to_pkg', { host: '1.2.3.4', password: '5536', outFile: 'roku-deploy-test' - });//TODO: fix! + }]); }); it('Deploys a package', async () => { @@ -284,7 +282,7 @@ describe('cli', () => { it('Prints device info to console', async () => { let consoleOutput = ''; sinon.stub(console, 'log').callsFake((...args) => { - consoleOutput += args.join(' ') + '\n'; //TODO: I don't think this is accurately adding a new line + consoleOutput += args.join(' ') + '\n'; }); sinon.stub(rokuDeploy, 'getDeviceInfo').returns(Promise.resolve({ 'device-id': '1234', @@ -293,10 +291,19 @@ describe('cli', () => { await new GetDeviceInfoCommand().run({ host: '1.2.3.4' }); - expect(consoleOutput.trim()).to.eql( - '{"device-id":"1234","serial-number":"abcd"}' - ); - }); //TODO: This passes when it is it.only, but fails in the larger test suite? + + // const consoleOutputObject: Record = { + // 'device-id': '1234', + // 'serial-number': 'abcd' + // }; + + expect(consoleOutput).to.eql([ + 'Name Value ', + '---------------------------', + 'device-id 1234 ', + 'serial-number abcd \n' + ].join('\n')); + }); it('Gets dev id', async () => { const stub = sinon.stub(rokuDeploy, 'getDevId').callsFake(async () => { diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 0670849..33f01f5 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -1,10 +1,10 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, toTable } from '../index'; export class GetDeviceInfoCommand { async run(args) { const outputPath = await rokuDeploy.getDeviceInfo({ host: args.host }); - console.log(JSON.stringify(outputPath)); + console.log(toTable(outputPath)); } } diff --git a/src/util.ts b/src/util.ts index cb90917..19eed65 100644 --- a/src/util.ts +++ b/src/util.ts @@ -255,3 +255,17 @@ export function standardizePathPosix(stringParts, ...expressions: any[]) { result.join('') ); } + +export function toTable(deviceInfo: Record) { + const margin = 5; + const keyWidth = Math.max(...Object.keys(deviceInfo).map(x => x.length)) + margin; + const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '')?.toString().length)) + margin; + let table = []; + table.push('Name'.padEnd(keyWidth, ' ') + 'Value'.padEnd(keyWidth, ' ')); + table.push('-'.repeat(keyWidth + valueWidth)); + for (const [key, value] of Object.entries(deviceInfo)) { + table.push(key.padEnd(keyWidth, ' ') + value?.toString().padEnd(keyWidth, ' ')); + } + + return table.join('\n'); +} //TODO: Create a test for this function From 09a275f9fedac4276b61919c259583cf8542d6b6 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Fri, 2 Feb 2024 12:08:24 -0500 Subject: [PATCH 12/63] Change name and input of table helper --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 19eed65..a1e22c0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -256,7 +256,7 @@ export function standardizePathPosix(stringParts, ...expressions: any[]) { ); } -export function toTable(deviceInfo: Record) { +export function printObjectToTable(deviceInfo: Record) { const margin = 5; const keyWidth = Math.max(...Object.keys(deviceInfo).map(x => x.length)) + margin; const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '')?.toString().length)) + margin; From e34ac976485170bf8c53b5b8a161d47ad0a926f2 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 2 Feb 2024 15:30:37 -0500 Subject: [PATCH 13/63] Totally broken code, but have the right names --- src/RokuDeploy.spec.ts | 114 ++-- src/RokuDeploy.ts | 556 +++++------------- src/cli.ts | 51 +- src/commands/DeleteInstalledChannelCommand.ts | 2 +- src/commands/PrepublishCommand.ts | 2 +- src/commands/PublishCommand.ts | 4 +- src/commands/SignExistingPackageCommand.ts | 2 +- src/commands/TakeScreenshotCommand.ts | 2 +- src/commands/ZipPackageCommand.ts | 2 +- src/index.ts | 12 +- src/util.ts | 240 ++++++++ 11 files changed, 508 insertions(+), 479 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 76d751a..53e76b2 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -31,6 +31,10 @@ describe('index', () => { beforeEach(() => { rokuDeploy = new RokuDeploy(); + + + rokuDeploy.convertToSquashfs + options = rokuDeploy.getOptions({ rootDir: rootDir, outDir: outDir, @@ -673,7 +677,7 @@ describe('index', () => { let err; try { fsExtra.ensureDirSync(options.stagingDir); - await rokuDeploy.zipPackage({ + await rokuDeploy.zip({ stagingDir: s`${tempDir}/path/to/nowhere`, outDir: outDir }); @@ -686,7 +690,7 @@ describe('index', () => { it('should throw error when manifest is missing and stagingDir does not exist', async () => { let err; try { - await rokuDeploy.zipPackage({ + await rokuDeploy.zip({ stagingDir: s`${tempDir}/path/to/nowhere`, outDir: outDir }); @@ -782,7 +786,7 @@ describe('index', () => { }); it('should retain the staging directory when told to', async () => { - let stagingDirValue = await rokuDeploy.prepublishToStaging({ + let stagingDirValue = await rokuDeploy.stage({ files: [ 'manifest' ], @@ -790,7 +794,7 @@ describe('index', () => { rootDir: rootDir }); expectPathExists(stagingDirValue); - await rokuDeploy.zipPackage({ + await rokuDeploy.zip({ stagingDir: stagingDir, retainStagingDir: true, outDir: outDir @@ -971,7 +975,7 @@ describe('index', () => { //the file should exist expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -987,7 +991,7 @@ describe('index', () => { //the file should exist expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1018,7 +1022,7 @@ describe('index', () => { //the file should exist expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1032,7 +1036,7 @@ describe('index', () => { it('fails when the zip file is missing', async () => { await expectThrowsAsync(async () => { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1046,7 +1050,7 @@ describe('index', () => { it('fails when no host is provided', () => { expectPathNotExists('rokudeploy.json'); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: undefined, password: 'password', outDir: outDir, @@ -1072,7 +1076,7 @@ describe('index', () => { }); try { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1091,7 +1095,7 @@ describe('index', () => { Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); `); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1110,7 +1114,7 @@ describe('index', () => { Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); `); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1127,7 +1131,7 @@ describe('index', () => { let body = 'Install Failure: Compilation Failed.'; mockDoPostRequest(body); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1144,7 +1148,7 @@ describe('index', () => { it('rejects when response contains invalid password status code', () => { mockDoPostRequest('', 401); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1161,7 +1165,7 @@ describe('index', () => { it('handles successful deploy', () => { mockDoPostRequest(); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1177,7 +1181,7 @@ describe('index', () => { it('handles successful deploy with remoteDebug', () => { const stub = mockDoPostRequest(); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1195,7 +1199,7 @@ describe('index', () => { it('handles successful deploy with remotedebug_connect_early', () => { const stub = mockDoPostRequest(); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1215,7 +1219,7 @@ describe('index', () => { let body = 'Identical to previous version -- not replacing.'; mockDoPostRequest(body); - return rokuDeploy.publish({ + return rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1233,7 +1237,7 @@ describe('index', () => { mockDoPostRequest(body, 123); try { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1251,7 +1255,7 @@ describe('index', () => { mockDoPostRequest('', 401); try { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1269,7 +1273,7 @@ describe('index', () => { mockDoPostRequest(null); try { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: '1.2.3.4', password: 'password', outDir: outDir, @@ -1517,7 +1521,7 @@ describe('index', () => { it('should return our error if signingPassword is not supplied', async () => { await expectThrowsAsync(async () => { - await rokuDeploy.signExistingPackage({ + await rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: undefined, @@ -1534,7 +1538,7 @@ describe('index', () => { process.nextTick(callback, error); return {} as any; }); - await rokuDeploy.signExistingPackage({ + await rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1550,7 +1554,7 @@ describe('index', () => { it('should return our error if it received invalid data', async () => { try { mockDoPostRequest(null); - await rokuDeploy.signExistingPackage({ + await rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1571,7 +1575,7 @@ describe('index', () => { mockDoPostRequest(body); await expectThrowsAsync( - rokuDeploy.signExistingPackage({ + rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1587,7 +1591,7 @@ describe('index', () => { node.appendChild(pkgDiv);`; mockDoPostRequest(body); - let pkgPath = await rokuDeploy.signExistingPackage({ + let pkgPath = await rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1599,7 +1603,7 @@ describe('index', () => { it('should return our fallback error if neither error or package link was detected', async () => { mockDoPostRequest(); await expectThrowsAsync( - rokuDeploy.signExistingPackage({ + rokuDeploy.signPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1612,7 +1616,7 @@ describe('index', () => { describe('prepublishToStaging', () => { it('should use outDir for staging folder', async () => { - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest' ], @@ -1622,7 +1626,7 @@ describe('index', () => { }); it('should support overriding the staging folder', async () => { - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: ['manifest'], stagingDir: `${tempDir}/custom-out-dir`, rootDir: rootDir @@ -1635,7 +1639,7 @@ describe('index', () => { 'manifest', 'source/main.brs' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', 'source/main.brs' @@ -1652,7 +1656,7 @@ describe('index', () => { 'manifest', 'source/main.brs' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', { @@ -1671,7 +1675,7 @@ describe('index', () => { 'manifest', 'source/main.brs' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ { src: 'manifest', @@ -1698,7 +1702,7 @@ describe('index', () => { 'manifest', 'source/main.brs' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ { src: 'manifest', @@ -1719,7 +1723,7 @@ describe('index', () => { writeFiles(rootDir, [ 'manifest' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ { src: sp`${rootDir}/manifest`, @@ -1743,7 +1747,7 @@ describe('index', () => { 'components/scenes/home/home.brs' ]); console.log('before'); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', 'components/!(scenes)/**/*' @@ -1763,7 +1767,7 @@ describe('index', () => { 'components/Loader/Loader.brs', 'components/scenes/Home/Home.brs' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', 'source', @@ -1780,7 +1784,7 @@ describe('index', () => { it('throws on invalid entries', async () => { try { - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', {} @@ -1797,7 +1801,7 @@ describe('index', () => { it('retains subfolder structure when referencing a folder', async () => { fsExtra.outputFileSync(`${rootDir}/flavors/shared/resources/images/fhd/image.jpg`, ''); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', { @@ -1817,7 +1821,7 @@ describe('index', () => { 'flavors/shared/resources/images/fhd/image.jpg', 'resources/image.jpg' ]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', { @@ -1919,7 +1923,7 @@ describe('index', () => { dest: s`renamed_test.md` }]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ rootDir: rootDir, stagingDir: stagingDir, files: [ @@ -1971,7 +1975,7 @@ describe('index', () => { dest: s`source/main.brs` }]); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ files: [ 'manifest', 'source/**/*' @@ -2004,7 +2008,7 @@ describe('index', () => { fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ rootDir: rootDir, stagingDir: stagingDir, files: [ @@ -2038,7 +2042,7 @@ describe('index', () => { fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); await expectThrowsAsync( - rokuDeploy.prepublishToStaging({ + rokuDeploy.stage({ rootDir: rootDir, stagingDir: stagingDir, files: [ @@ -2250,7 +2254,7 @@ describe('index', () => { it('attempts to delete any installed dev channel on the device', async () => { mockDoPostRequest(); - let result = await rokuDeploy.deleteInstalledChannel({ + let result = await rokuDeploy.deleteDevChannel({ host: '1.2.3.4', password: 'password' }); @@ -2301,19 +2305,19 @@ describe('index', () => { `); mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); }); it('throws when there is no response body', async () => { // missing body mockDoPostRequest(null); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); }); it('throws when there is an empty response body', async () => { // empty body mockDoPostRequest(); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); }); it('throws when there is an error downloading the image from device', async () => { @@ -2334,7 +2338,7 @@ describe('index', () => { }; mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); }); it('handles the device returning a png', async () => { @@ -2355,7 +2359,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); expect(result).not.to.be.undefined; expect(path.extname(result)).to.equal('.png'); expect(fsExtra.existsSync(result)); @@ -2379,7 +2383,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); expect(result).not.to.be.undefined; expect(path.extname(result)).to.equal('.jpg'); expect(fsExtra.existsSync(result)); @@ -2403,7 +2407,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: `${tempDir}/myScreenShots` }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: `${tempDir}/myScreenShots` }); expect(result).not.to.be.undefined; expect(util.standardizePath(`${tempDir}/myScreenShots`)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(result)); @@ -2427,7 +2431,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my' }); expect(result).not.to.be.undefined; expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(path.join(tempDir, 'my.png'))); @@ -2451,7 +2455,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my.jpg' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my.jpg' }); expect(result).not.to.be.undefined; expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(path.join(tempDir, 'my.jpg.png'))); @@ -2475,7 +2479,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); expect(result).not.to.be.undefined; expect(fsExtra.existsSync(result)); }); @@ -2498,7 +2502,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outFile: 'myFile' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outFile: 'myFile' }); expect(result).not.to.be.undefined; expect(path.basename(result)).to.equal('myFile.jpg'); expect(fsExtra.existsSync(result)); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 4438f21..28e1059 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -26,86 +26,56 @@ export class RokuDeploy { } private logger: Logger; - //store the import on the class to make testing easier - public fsExtra = _fsExtra; - - public screenshotDir = path.join(tempDir, '/roku-deploy/screenshots/'); + //this should just + // public screenshotDir = path.join(tempDir, '/roku-deploy/screenshots/'); /** * Copies all of the referenced files to the staging folder * @param options */ - public async prepublishToStaging(options: PrepublishToStagingOptions) { + public async stage(options: StageOptions) { options = this.getOptions(options) as any; //clean the staging directory - await this.fsExtra.remove(options.stagingDir); + await fsExtra.remove(options.stagingDir); //make sure the staging folder exists - await this.fsExtra.ensureDir(options.stagingDir); - await this.copyToStaging(options.files, options.stagingDir, options.rootDir); - return options.stagingDir; - } - - /** - * Given an array of `FilesType`, normalize them each into a `StandardizedFileEntry`. - * Each entry in the array or inner `src` array will be extracted out into its own object. - * This makes it easier to reason about later on in the process. - * @param files - */ - public normalizeFilesArray(files: FileEntry[]) { - const result: Array = []; - - for (let i = 0; i < files.length; i++) { - let entry = files[i]; - //skip falsey and blank entries - if (!entry) { - continue; - - //string entries - } else if (typeof entry === 'string') { - result.push(entry); - - //objects with src: (string | string[]) - } else if ('src' in entry) { - //validate dest - if (entry.dest !== undefined && entry.dest !== null && typeof entry.dest !== 'string') { - throw new Error(`Invalid type for "dest" at index ${i} of files array`); - } + await fsExtra.ensureDir(options.stagingDir); + // await this.copyToStaging(options.files, options.stagingDir, options.rootDir); - //objects with src: string - if (typeof entry.src === 'string') { - result.push({ - src: entry.src, - dest: util.standardizePath(entry.dest) - }); - - //objects with src:string[] - } else if ('src' in entry && Array.isArray(entry.src)) { - //create a distinct entry for each item in the src array - for (let srcEntry of entry.src) { - result.push({ - src: srcEntry, - dest: util.standardizePath(entry.dest) - }); - } - } else { - throw new Error(`Invalid type for "src" at index ${i} of files array`); - } - } else { - throw new Error(`Invalid entry at index ${i} in files array`); - } + if (!stagingPath) { + throw new Error('stagingPath is required'); + } + if (!await fsExtra.pathExists(rootDir)) { + throw new Error(`rootDir does not exist at "${rootDir}"`); } - return result; + let fileObjects = await this.getFilePaths(options.files, rootDir); + //copy all of the files + await Promise.all(fileObjects.map(async (fileObject) => { + let destFilePath = util.standardizePath(`${stagingPath}/${fileObject.dest}`); + + //make sure the containing folder exists + await this.fsExtra.ensureDir(path.dirname(destFilePath)); + + //sometimes the copyfile action fails due to race conditions (normally to poorly constructed src;dest; objects with duplicate files in them + await util.tryRepeatAsync(async () => { + //copy the src item using the filesystem + await this.fsExtra.copy(fileObject.src, destFilePath, { + //copy the actual files that symlinks point to, not the symlinks themselves + dereference: true + }); + }, 10); + })); + return options.stagingDir; } /** * Given an already-populated staging folder, create a zip archive of it and copy it to the output folder * @param options */ - public async zipPackage(options: ZipPackageOptions) { + public async zip(options: ZipPackageOptions) { options = this.getOptions(options) as any; //make sure the output folder exists @@ -119,260 +89,44 @@ export class RokuDeploy { } //create a zip of the staging folder - await this.zipFolder(options.stagingDir, zipFilePath); - - //delete the staging folder unless told to retain it. - if (options.retainStagingDir !== true) { - await this.fsExtra.remove(options.stagingDir); - } - } - - /** - * Create a zip folder containing all of the specified roku project files. - * @param options - */ - public async createPackage(options: CreatePackageOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => Promise | void) { - options = this.getOptions(options) as any; - - await this.prepublishToStaging(options); - - let manifestPath = util.standardizePath(`${options.stagingDir}/manifest`); - let parsedManifest = await this.parseManifest(manifestPath); - - if (options.incrementBuildNumber) { - let timestamp = dateformat(new Date(), 'yymmddHHMM'); - parsedManifest.build_version = timestamp; //eslint-disable-line camelcase - await this.fsExtra.writeFile(manifestPath, this.stringifyManifest(parsedManifest)); - } - - if (beforeZipCallback) { - let info: BeforeZipCallbackInfo = { - manifestData: parsedManifest, - stagingFolderPath: options.stagingDir, - stagingDir: options.stagingDir - }; - - await Promise.resolve(beforeZipCallback(info)); - } - await this.zipPackage(options); + await this.__makeZip(options.stagingDir, zipFilePath); } /** - * Given a root directory, normalize it to a full path. - * Fall back to cwd if not specified - * @param rootDir - */ - public normalizeRootDir(rootDir: string) { - if (!rootDir || (typeof rootDir === 'string' && rootDir.trim().length === 0)) { - return process.cwd(); - } else { - return path.resolve(rootDir); - } - } - - /** - * Get all file paths for the specified options - * @param files - * @param rootFolderPath - the absolute path to the root dir where relative files entries are relative to - */ - public async getFilePaths(files: FileEntry[], rootDir: string): Promise { - //if the rootDir isn't absolute, convert it to absolute using the standard options flow - if (path.isAbsolute(rootDir) === false) { - rootDir = this.getOptions({ rootDir: rootDir }).rootDir; - } - const entries = this.normalizeFilesArray(files); - const srcPathsByIndex = await util.globAllByIndex( - entries.map(x => { - return typeof x === 'string' ? x : x.src; - }), - rootDir - ); - - /** - * Result indexed by the dest path - */ - let result = new Map(); - - //compute `dest` path for every file - for (let i = 0; i < srcPathsByIndex.length; i++) { - const srcPaths = srcPathsByIndex[i]; - const entry = entries[i]; - if (srcPaths) { - for (let srcPath of srcPaths) { - srcPath = util.standardizePath(srcPath); - - const dest = this.computeFileDestPath(srcPath, entry, rootDir); - //the last file with this `dest` will win, so just replace any existing entry with this one. - result.set(dest, { - src: srcPath, - dest: dest - }); - } - } - } - return [...result.values()]; - } - - /** - * Given a full path to a file, determine its dest path - * @param srcPath the absolute path to the file. This MUST be a file path, and it is not verified to exist on the filesystem - * @param files the files array - * @param rootDir the absolute path to the root dir - * @param skipMatch - skip running the minimatch process (i.e. assume the file is a match - * @returns the RELATIVE path to the dest location for the file. + * Given a path to a folder, zip up that folder and all of its contents + * @param srcFolder the folder that should be zipped + * @param zipFilePath the path to the zip that will be created + * @param preZipCallback a function to call right before every file gets added to the zip + * @param files a files array used to filter the files from `srcFolder` */ - public getDestPath(srcPathAbsolute: string, files: FileEntry[], rootDir: string, skipMatch = false) { - srcPathAbsolute = util.standardizePath(srcPathAbsolute); - rootDir = rootDir.replace(/\\+/g, '/'); - const entries = this.normalizeFilesArray(files); - - function makeGlobAbsolute(pattern: string) { - return path.resolve( - path.posix.join( - rootDir, - //remove leading exclamation point if pattern is negated - pattern - //coerce all slashes to forward - ) - ).replace(/\\/g, '/'); - } - - let result: string; + private async __makeZip(srcFolder: string, zipFilePath: string, preFileZipCallback?: (file: StandardizedFileEntry, data: Buffer) => Buffer, files: FileEntry[] = ['**/*']) { + const filePaths = await util.getFilePaths(files, srcFolder); - //add the file into every matching cache bucket - for (let entry of entries) { - const pattern = (typeof entry === 'string' ? entry : entry.src); - //filter previous paths - if (pattern.startsWith('!')) { - const keepFile = picomatch('!' + makeGlobAbsolute(pattern.replace(/^!/, ''))); - if (!keepFile(srcPathAbsolute)) { - result = undefined; - } - } else { - const keepFile = picomatch(makeGlobAbsolute(pattern)); - if (keepFile(srcPathAbsolute)) { - try { - result = this.computeFileDestPath( - srcPathAbsolute, - entry, - util.standardizePath(rootDir) - ); - } catch { - //ignore errors...the file just has no dest path - } + const zip = new JSZip(); + // Allows us to wait until all are done before we build the zip + const promises = []; + for (const file of filePaths) { + const promise = this.fsExtra.readFile(file.src).then((data) => { + if (preFileZipCallback) { + data = preFileZipCallback(file, data); } - } - } - return result; - } - /** - * Compute the `dest` path. This accounts for magic globstars in the pattern, - * as well as relative paths based on the dest. This is only used internally. - * @param src an absolute, normalized path for a file - * @param dest the `dest` entry for this file. If omitted, files will derive their paths relative to rootDir. - * @param pattern the glob pattern originally used to find this file - * @param rootDir absolute normalized path to the rootDir - */ - private computeFileDestPath(srcPath: string, entry: string | StandardizedFileEntry, rootDir: string) { - let result: string; - let globstarIdx: number; - //files under rootDir with no specified dest - if (typeof entry === 'string') { - if (util.isParentOfPath(rootDir, srcPath, false)) { - //files that are actually relative to rootDir - result = util.stringReplaceInsensitive(srcPath, rootDir, ''); - } else { - // result = util.stringReplaceInsensitive(srcPath, rootDir, ''); - throw new Error('Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); - } - - //non-glob-pattern explicit file reference - } else if (!isGlob(entry.src.replace(/\\/g, '/'), { strict: false })) { - let isEntrySrcAbsolute = path.isAbsolute(entry.src); - let entrySrcPathAbsolute = isEntrySrcAbsolute ? entry.src : util.standardizePath(`${rootDir}/${entry.src}`); - - let isSrcChildOfRootDir = util.isParentOfPath(rootDir, entrySrcPathAbsolute, false); - - let fileNameAndExtension = path.basename(entrySrcPathAbsolute); - - //no dest - if (entry.dest === null || entry.dest === undefined) { - //no dest, absolute path or file outside of rootDir - if (isEntrySrcAbsolute || isSrcChildOfRootDir === false) { - //copy file to root of staging folder - result = fileNameAndExtension; + const ext = path.extname(file.dest).toLowerCase(); + let compression = 'DEFLATE'; - //no dest, relative path, lives INSIDE rootDir - } else { - //copy relative file structure to root of staging folder - let srcPathRelative = util.stringReplaceInsensitive(entrySrcPathAbsolute, rootDir, ''); - result = srcPathRelative; + if (ext === '.jpg' || ext === '.png' || ext === '.jpeg') { + compression = 'STORE'; } - - //assume entry.dest is the relative path to the folder AND file if applicable - } else if (entry.dest === '') { - result = fileNameAndExtension; - } else { - result = entry.dest; - } - //has a globstar - } else if ((globstarIdx = entry.src.indexOf('**')) > -1) { - const rootGlobstarPath = path.resolve(rootDir, entry.src.substring(0, globstarIdx)) + path.sep; - const srcPathRelative = util.stringReplaceInsensitive(srcPath, rootGlobstarPath, ''); - if (entry.dest) { - result = `${entry.dest}/${srcPathRelative}`; - } else { - result = srcPathRelative; - } - - //`pattern` is some other glob magic - } else { - const fileNameAndExtension = path.basename(srcPath); - if (entry.dest) { - result = util.standardizePath(`${entry.dest}/${fileNameAndExtension}`); - } else { - result = util.stringReplaceInsensitive(srcPath, rootDir, ''); - } - } - - result = util.standardizePath( - //remove leading slashes - result.replace(/^[\/\\]+/, '') - ); - return result; - } - - /** - * Copy all of the files to the staging directory - * @param fileGlobs - * @param stagingPath - */ - private async copyToStaging(files: FileEntry[], stagingPath: string, rootDir: string) { - if (!stagingPath) { - throw new Error('stagingPath is required'); - } - if (!await this.fsExtra.pathExists(rootDir)) { - throw new Error(`rootDir does not exist at "${rootDir}"`); - } - - let fileObjects = await this.getFilePaths(files, rootDir); - //copy all of the files - await Promise.all(fileObjects.map(async (fileObject) => { - let destFilePath = util.standardizePath(`${stagingPath}/${fileObject.dest}`); - - //make sure the containing folder exists - await this.fsExtra.ensureDir(path.dirname(destFilePath)); - - //sometimes the copyfile action fails due to race conditions (normally to poorly constructed src;dest; objects with duplicate files in them - await util.tryRepeatAsync(async () => { - //copy the src item using the filesystem - await this.fsExtra.copy(fileObject.src, destFilePath, { - //copy the actual files that symlinks point to, not the symlinks themselves - dereference: true + zip.file(file.dest.replace(/[\\/]/g, '/'), data, { + compression: compression }); - }, 10); - })); + }); + promises.push(promise); + } + await Promise.all(promises); + // level 2 compression seems to be the best balance between speed and file size. Speed matters more since most will be calling squashfs afterwards. + const content = await zip.generateAsync({ type: 'nodebuffer', compressionOptions: { level: 2 } }); + return this.fsExtra.writeFile(zipFilePath, content); } private generateBaseRequestOptions(requestPath: string, options: BaseRequestOptions, formData = {} as T): requestType.OptionsWithUrl { @@ -392,6 +146,37 @@ export class RokuDeploy { return baseRequestOptions; } + public async keypress(options: { key: string }) { + return this.sendKeyEvent({ + ...options, + action: 'keypress' + }); + } + + public async keyup(options: any) { + return this.sendKeyEvent({ + ...options, + action: 'keyup' + }); + } + + public async keydown(options: any) { + return this.sendKeyEvent({ + ...options, + action: 'keydown' + }); + } + + public async sendText(options: any) { + const chars = options.text.split(''); + for (const char of chars) { + await this.keypress({ + ...options, + key: `lit_${char}` + }); + } + } + /** * Simulate pressing the home button on the remote for this roku. * This makes the roku return to the home screen @@ -399,7 +184,7 @@ export class RokuDeploy { * @param port - the port that should be used for the request. defaults to 8060 * @param timeout - request timeout duration in milliseconds. defaults to 150000 */ - public async pressHomeButton(host, port?: number, timeout?: number) { + private async sendKeyEvent(options: { host: string; port?: string; key: 'home' | 'left' | 'all.the.others'; action: 'keypress' | 'keyup' | 'keydown'; timeout?: number }) { let options = this.getOptions(); port = port ? port : options.remotePort; timeout = timeout ? timeout : options.timeout; @@ -410,11 +195,18 @@ export class RokuDeploy { }, false); } + public async closeChannel(options: CloseAppOptions) { + //TODO + + //if supports ecp close-app, then do that (twice so it kills instant resume) + //else, send home press + } + /** * Publish a pre-existing packaged zip file to a remote Roku. * @param options */ - public async publish(options: PublishOptions): Promise<{ message: string; results: any }> { + public async sideload(options: PublishOptions): Promise<{ message: string; results: any }> { options = this.getOptions(options) as any; if (!options.host) { throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); @@ -423,6 +215,15 @@ export class RokuDeploy { await this.fsExtra.ensureDir(options.outDir); let zipFilePath = this.getOutputZipFilePath(options as any); + + if (options.deleteInstalledChannel) { + try { + await this.deleteDevChannel(options); + } catch (e) { + // note we don't report the error; as we don't actually care that we could not deploy - it's just useless noise to log it. + } + } + let readStream: _fsExtra.ReadStream; try { if ((await this.fsExtra.pathExists(zipFilePath)) === false) { @@ -496,7 +297,7 @@ export class RokuDeploy { } /** - * Converts existing loaded package to squashfs for faster loading packages + * Converts the currently sideloaded dev app to squashfs for faster loading packages * @param options */ public async convertToSquashfs(options: ConvertToSquashfsOptions) { @@ -572,7 +373,7 @@ export class RokuDeploy { * Sign a pre-existing package using Roku and return path to retrieve it * @param options */ - public async signExistingPackage(options: SignExistingPackageOptions): Promise { + public async createSignedPackage(options: SignExistingPackageOptions): Promise { options = this.getOptions(options) as any; if (!options.signingPassword) { throw new errors.MissingRequiredOptionError('Must supply signingPassword'); @@ -581,6 +382,11 @@ export class RokuDeploy { let parsedManifest = await this.parseManifest(manifestPath); let appName = parsedManifest.title + '/' + parsedManifest.major_version + '.' + parsedManifest.minor_version; + //prevent devId mismatch (if devId is specified) + if (options.devId && options.devId !== await this.getDevId()) { + throw new Error('devId mismatch. nope, not gonna sign'); + } + let requestOptions = this.generateBaseRequestOptions('plugin_package', options as any, { mysubmit: 'Package', pkg_time: (new Date()).getTime(), //eslint-disable-line camelcase @@ -597,25 +403,17 @@ export class RokuDeploy { let pkgSearchMatches = //.exec(results.body); if (pkgSearchMatches) { - return pkgSearchMatches[1]; + const url = pkgSearchMatches[1]; + options = this.getOptions(options) as any; + let requestOptions2 = this.generateBaseRequestOptions(url, options); + + let pkgFilePath = this.getOutputPkgFilePath(options as any); + return this.getToFile(requestOptions2, pkgFilePath); } throw new errors.UnknownDeviceResponseError('Unknown error signing package', results); } - /** - * Sign a pre-existing package using Roku and return path to retrieve it - * @param pkgPath - * @param options - */ - public async retrieveSignedPackage(pkgPath: string, options: RetrieveSignedPackageOptions): Promise { - options = this.getOptions(options) as any; - let requestOptions = this.generateBaseRequestOptions(pkgPath, options); - - let pkgFilePath = this.getOutputPkgFilePath(options as any); - return this.getToFile(requestOptions, pkgFilePath); - } - /** * Centralized function for handling POST http requests * @param params @@ -757,29 +555,11 @@ export class RokuDeploy { return result; } - /** - * Create a zip of the project, and then publish to the target Roku device - * @param options - */ - public async deploy(options?: DeployOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => void) { - options = this.getOptions(options) as any; - await this.createPackage(options, beforeZipCallback); - if (options.deleteInstalledChannel) { - try { - await this.deleteInstalledChannel(options); - } catch (e) { - // note we don't report the error; as we don't actually care that we could not deploy - it's just useless noise to log it. - } - } - let result = await this.publish(options as any); - return result; - } - /** * Deletes any installed dev channel on the target Roku device * @param options */ - public async deleteInstalledChannel(options?: DeleteInstalledChannelOptions) { + public async deleteDevChannel(options?: DeleteInstalledChannelOptions) { options = this.getOptions(options) as any; let deleteOptions = this.generateBaseRequestOptions('plugin_install', options as any); @@ -793,7 +573,7 @@ export class RokuDeploy { /** * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ - public async takeScreenshot(options: TakeScreenshotOptions) { + public async captureScreenshot(options: TakeScreenshotOptions) { options.outDir = options.outDir ?? this.screenshotDir; options.outFile = options.outFile ?? `screenshot-${dayjs().format('YYYY-MM-DD-HH.mm.ss.SSS')}`; let saveFilePath: string; @@ -857,33 +637,11 @@ export class RokuDeploy { }); } - /** - * executes sames steps as deploy and signs the package and stores it in the out folder - * @param options - */ - public async deployAndSignPackage(options?: DeployAndSignPackageOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => void): Promise { - options = this.getOptions(options) as any; - let retainStagingDirInitialValue = options.retainStagingDir; - options.retainStagingDir = true; - await this.deploy(options as any, beforeZipCallback); - - if (options.convertToSquashfs) { - await this.convertToSquashfs(options as any); - } - - let remotePkgPath = await this.signExistingPackage(options as any); - let localPkgFilePath = await this.retrieveSignedPackage(remotePkgPath, options as any); - if (retainStagingDirInitialValue !== true) { - await this.fsExtra.remove(options.stagingDir); - } - return localPkgFilePath; - } - /** * Get an options with all overridden vaues, and then defaults for missing values * @param options */ - public getOptions(options: RokuDeployOptions = {}) { + private __getOptions(options: RokuDeployOptions = {}) { let fileOptions: RokuDeployOptions = {}; const fileNames = ['rokudeploy.json', 'bsconfig.json']; if (options.project) { @@ -959,7 +717,7 @@ export class RokuDeploy { * Centralizes getting output zip file path based on passed in options * @param options */ - public getOutputZipFilePath(options?: GetOutputZipFilePathOptions) { + private getOutputZipFilePath(options?: GetOutputZipFilePathOptions) { options = this.getOptions(options) as any; let zipFileName = options.outFile; @@ -976,7 +734,7 @@ export class RokuDeploy { * Centralizes getting output pkg file path based on passed in options * @param options */ - public getOutputPkgFilePath(options?: GetOutputPkgFilePathOptions) { + private getOutputPkgFilePath(options?: GetOutputPkgFilePathOptions) { options = this.getOptions(options) as any; let pkgFileName = options.outFile; @@ -1045,7 +803,7 @@ export class RokuDeploy { * decoding HtmlEntities, etc. * @param deviceInfo */ - public normalizeDeviceInfoFieldValue(value: any) { + private normalizeDeviceInfoFieldValue(value: any) { let num: number; // convert 'true' and 'false' string values to boolean if (value === 'true') { @@ -1059,12 +817,20 @@ export class RokuDeploy { } } + /** + * TODO we might delete this one. let chris think about it. ;) + * @param options + * @returns + */ public async getDevId(options?: GetDevIdOptions) { const deviceInfo = await this.getDeviceInfo(options); return deviceInfo['keyed-developer-id']; } - public async parseManifest(manifestPath: string): Promise { + /** + * TODO move these manifest functions to a util somewhere + */ + private async parseManifest(manifestPath: string): Promise { if (!await this.fsExtra.pathExists(manifestPath)) { throw new Error(manifestPath + ' does not exist'); } @@ -1073,7 +839,10 @@ export class RokuDeploy { return this.parseManifestFromString(manifestContents); } - public parseManifestFromString(manifestContents: string): ManifestData { + /** + * TODO move these manifest functions to a util somewhere + */ + private parseManifestFromString(manifestContents: string): ManifestData { let manifestLines = manifestContents.split('\n'); let manifestData: ManifestData = {}; manifestData.keyIndexes = {}; @@ -1090,6 +859,9 @@ export class RokuDeploy { return manifestData; } + /** + * TODO move these manifest functions to a util somewhere + */ public stringifyManifest(manifestData: ManifestData): string { let output = []; @@ -1113,43 +885,6 @@ export class RokuDeploy { return output.join('\n'); } - - /** - * Given a path to a folder, zip up that folder and all of its contents - * @param srcFolder the folder that should be zipped - * @param zipFilePath the path to the zip that will be created - * @param preZipCallback a function to call right before every file gets added to the zip - * @param files a files array used to filter the files from `srcFolder` - */ - public async zipFolder(srcFolder: string, zipFilePath: string, preFileZipCallback?: (file: StandardizedFileEntry, data: Buffer) => Buffer, files: FileEntry[] = ['**/*']) { - const filePaths = await this.getFilePaths(files, srcFolder); - - const zip = new JSZip(); - // Allows us to wait until all are done before we build the zip - const promises = []; - for (const file of filePaths) { - const promise = this.fsExtra.readFile(file.src).then((data) => { - if (preFileZipCallback) { - data = preFileZipCallback(file, data); - } - - const ext = path.extname(file.dest).toLowerCase(); - let compression = 'DEFLATE'; - - if (ext === '.jpg' || ext === '.png' || ext === '.jpeg') { - compression = 'STORE'; - } - zip.file(file.dest.replace(/[\\/]/g, '/'), data, { - compression: compression - }); - }); - promises.push(promise); - } - await Promise.all(promises); - // level 2 compression seems to be the best balance between speed and file size. Speed matters more since most will be calling squashfs afterwards. - const content = await zip.generateAsync({ type: 'nodebuffer', compressionOptions: { level: 2 } }); - return this.fsExtra.writeFile(zipFilePath, content); - } } export interface ManifestData { @@ -1253,7 +988,7 @@ export interface GetDeviceInfoOptions { enhance?: boolean; } -export interface PrepublishToStagingOptions { +export interface StageOptions { rootDir?: string; files?: FileEntry[]; stagingDir?: string; @@ -1262,7 +997,6 @@ export interface PrepublishToStagingOptions { export interface ZipPackageOptions { stagingDir?: string; - retainStagingDir?: boolean; outDir?: string; } @@ -1313,6 +1047,10 @@ export interface SignExistingPackageOptions { password: string; signingPassword: string; stagingDir?: string; + /** + * If specified, signing will fail if the device's devId is different than this value + */ + devId?: string; } export interface RetrieveSignedPackageOptions { diff --git a/src/cli.ts b/src/cli.ts index 002551f..d54fcbc 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,7 +18,52 @@ import { GetDevIdCommand } from './commands/GetDevIdCommand'; import { ZipFolderCommand } from './commands/ZipFolderCommand'; void yargs - .command('prepublishToStaging', 'Copies all of the referenced files to the staging folder', (builder) => { + + //not exposed + // .command('stage') + // .command('zip') + // .command('closeChannel') + // .command('sideload') + // .command('convertToSquashfs') //alias: squash + // .command('rekeyDevice') //alias: rekey + // .command('createSignedPackage') //alias: sign + // .command('deleteDevChannel') // alias: rmdev deldev + + .command('keypress') + .command('keyup') + .command('keydown') + .command('text') //alias: sendText + .command('screenshot') // alias: captureScreenshot + .command('deviceinfo') // alias: getDeviceInfo + .command('devid') // alias: getDevId + + //bundle + .command('stage|zip') + + //deploy + .command('stage|zip|delete|close|sideload') + + //package + .command('close|rekey|stage|zip|delete|close|sideload|squash|sign') + + //exec + .command('magic') + + + + + + + + + + + + + + + + .command('stage', 'Copies all of the referenced files to the staging folder', (builder) => { return builder .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }); @@ -26,7 +71,7 @@ void yargs return new PrepublishCommand().run(args); }) - .command('zipPackage', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { + .command('zip', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { return builder .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); @@ -34,6 +79,8 @@ void yargs return new ZipPackageCommand().run(args); }) + + .command('createPackage', 'Create a zip folder containing all of the specified roku project files', (builder) => { return builder .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) diff --git a/src/commands/DeleteInstalledChannelCommand.ts b/src/commands/DeleteInstalledChannelCommand.ts index 1ec9c29..b6cfebc 100644 --- a/src/commands/DeleteInstalledChannelCommand.ts +++ b/src/commands/DeleteInstalledChannelCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class DeleteInstalledChannelCommand { async run(args) { - await rokuDeploy.deleteInstalledChannel({ + await rokuDeploy.deleteDevChannel({ host: args.host, password: args.password }); diff --git a/src/commands/PrepublishCommand.ts b/src/commands/PrepublishCommand.ts index 2ca65cc..48152ed 100644 --- a/src/commands/PrepublishCommand.ts +++ b/src/commands/PrepublishCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class PrepublishCommand { async run(args) { - await rokuDeploy.prepublishToStaging({ + await rokuDeploy.stage({ stagingDir: args.stagingDir, rootDir: args.rootDir }); diff --git a/src/commands/PublishCommand.ts b/src/commands/PublishCommand.ts index 91554fa..7338471 100644 --- a/src/commands/PublishCommand.ts +++ b/src/commands/PublishCommand.ts @@ -2,11 +2,11 @@ import { rokuDeploy } from '../index'; export class PublishCommand { async run(args) { - await rokuDeploy.publish({ + await rokuDeploy.sideload({ host: args.host, password: args.password, outDir: args.outDir, outFile: args.outFile }); } -} \ No newline at end of file +} diff --git a/src/commands/SignExistingPackageCommand.ts b/src/commands/SignExistingPackageCommand.ts index 2d5733f..3b1175e 100644 --- a/src/commands/SignExistingPackageCommand.ts +++ b/src/commands/SignExistingPackageCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class SignExistingPackageCommand { async run(args) { - await rokuDeploy.signExistingPackage({ + await rokuDeploy.signPackage({ host: args.host, password: args.password, signingPassword: args.signingPassword, diff --git a/src/commands/TakeScreenshotCommand.ts b/src/commands/TakeScreenshotCommand.ts index ac5b7bc..2d9f358 100644 --- a/src/commands/TakeScreenshotCommand.ts +++ b/src/commands/TakeScreenshotCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class TakeScreenshotCommand { async run(args) { - await rokuDeploy.takeScreenshot({ + await rokuDeploy.captureScreenshot({ host: args.host, password: args.password }); diff --git a/src/commands/ZipPackageCommand.ts b/src/commands/ZipPackageCommand.ts index 7ef1f09..4cd0ebf 100644 --- a/src/commands/ZipPackageCommand.ts +++ b/src/commands/ZipPackageCommand.ts @@ -2,7 +2,7 @@ import { rokuDeploy } from '../index'; export class ZipPackageCommand { async run(args) { - await rokuDeploy.zipPackage({ + await rokuDeploy.zip({ stagingDir: args.stagingDir, outDir: args.outDir }); diff --git a/src/index.ts b/src/index.ts index d68b4d5..cb6007a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ export * from './DeviceInfo'; export const rokuDeploy = new RokuDeploy(); let createPackage = RokuDeploy.prototype.createPackage.bind(rokuDeploy); -let deleteInstalledChannel = RokuDeploy.prototype.deleteInstalledChannel.bind(rokuDeploy); +let deleteInstalledChannel = RokuDeploy.prototype.deleteDevChannel.bind(rokuDeploy); let deploy = RokuDeploy.prototype.deploy.bind(rokuDeploy); let deployAndSignPackage = RokuDeploy.prototype.deployAndSignPackage.bind(rokuDeploy); let getDestPath = RokuDeploy.prototype.getDestPath.bind(rokuDeploy); @@ -23,17 +23,17 @@ let getOutputZipFilePath = RokuDeploy.prototype.getOutputZipFilePath.bind(rokuDe let normalizeFilesArray = RokuDeploy.prototype.normalizeFilesArray.bind(rokuDeploy); let normalizeRootDir = RokuDeploy.prototype.normalizeRootDir.bind(rokuDeploy); let parseManifest = RokuDeploy.prototype.parseManifest.bind(rokuDeploy); -let prepublishToStaging = RokuDeploy.prototype.prepublishToStaging.bind(rokuDeploy); +let prepublishToStaging = RokuDeploy.prototype.stage.bind(rokuDeploy); let pressHomeButton = RokuDeploy.prototype.pressHomeButton.bind(rokuDeploy); -let publish = RokuDeploy.prototype.publish.bind(rokuDeploy); +let publish = RokuDeploy.prototype.sideload.bind(rokuDeploy); let rekeyDevice = RokuDeploy.prototype.rekeyDevice.bind(rokuDeploy); let retrieveSignedPackage = RokuDeploy.prototype.retrieveSignedPackage.bind(rokuDeploy); -let signExistingPackage = RokuDeploy.prototype.signExistingPackage.bind(rokuDeploy); +let signExistingPackage = RokuDeploy.prototype.signPackage.bind(rokuDeploy); let stringifyManifest = RokuDeploy.prototype.stringifyManifest.bind(rokuDeploy); -let takeScreenshot = RokuDeploy.prototype.takeScreenshot.bind(rokuDeploy); +let takeScreenshot = RokuDeploy.prototype.captureScreenshot.bind(rokuDeploy); let getDevId = RokuDeploy.prototype.getDevId.bind(rokuDeploy); let zipFolder = RokuDeploy.prototype.zipFolder.bind(rokuDeploy); -let zipPackage = RokuDeploy.prototype.zipPackage.bind(rokuDeploy); +let zipPackage = RokuDeploy.prototype.zip.bind(rokuDeploy); export { createPackage, diff --git a/src/util.ts b/src/util.ts index a1e22c0..f5a92f8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -225,6 +225,246 @@ export class Util { }); } + /** + * Given an array of `FilesType`, normalize them each into a `StandardizedFileEntry`. + * Each entry in the array or inner `src` array will be extracted out into its own object. + * This makes it easier to reason about later on in the process. + * @param files + */ + public normalizeFilesArray(files: FileEntry[]) { + const result: Array = []; + + for (let i = 0; i < files.length; i++) { + let entry = files[i]; + //skip falsey and blank entries + if (!entry) { + continue; + + //string entries + } else if (typeof entry === 'string') { + result.push(entry); + + //objects with src: (string | string[]) + } else if ('src' in entry) { + //validate dest + if (entry.dest !== undefined && entry.dest !== null && typeof entry.dest !== 'string') { + throw new Error(`Invalid type for "dest" at index ${i} of files array`); + } + + //objects with src: string + if (typeof entry.src === 'string') { + result.push({ + src: entry.src, + dest: util.standardizePath(entry.dest) + }); + + //objects with src:string[] + } else if ('src' in entry && Array.isArray(entry.src)) { + //create a distinct entry for each item in the src array + for (let srcEntry of entry.src) { + result.push({ + src: srcEntry, + dest: util.standardizePath(entry.dest) + }); + } + } else { + throw new Error(`Invalid type for "src" at index ${i} of files array`); + } + } else { + throw new Error(`Invalid entry at index ${i} in files array`); + } + } + + return result; + } + + + /** + * Get all file paths for the specified options + * @param files + * @param rootFolderPath - the absolute path to the root dir where relative files entries are relative to + */ + public async getFilePaths(files: FileEntry[], rootDir: string): Promise { + //if the rootDir isn't absolute, convert it to absolute using the standard options flow + if (path.isAbsolute(rootDir) === false) { + rootDir = this.getOptions({ rootDir: rootDir }).rootDir; + } + const entries = this.normalizeFilesArray(files); + const srcPathsByIndex = await util.globAllByIndex( + entries.map(x => { + return typeof x === 'string' ? x : x.src; + }), + rootDir + ); + + /** + * Result indexed by the dest path + */ + let result = new Map(); + + //compute `dest` path for every file + for (let i = 0; i < srcPathsByIndex.length; i++) { + const srcPaths = srcPathsByIndex[i]; + const entry = entries[i]; + if (srcPaths) { + for (let srcPath of srcPaths) { + srcPath = util.standardizePath(srcPath); + + const dest = this.computeFileDestPath(srcPath, entry, rootDir); + //the last file with this `dest` will win, so just replace any existing entry with this one. + result.set(dest, { + src: srcPath, + dest: dest + }); + } + } + } + return [...result.values()]; + } + + /** + * Given a full path to a file, determine its dest path + * @param srcPath the absolute path to the file. This MUST be a file path, and it is not verified to exist on the filesystem + * @param files the files array + * @param rootDir the absolute path to the root dir + * @param skipMatch - skip running the minimatch process (i.e. assume the file is a match + * @returns the RELATIVE path to the dest location for the file. + */ + public getDestPath(srcPathAbsolute: string, files: FileEntry[], rootDir: string, skipMatch = false) { + srcPathAbsolute = util.standardizePath(srcPathAbsolute); + rootDir = rootDir.replace(/\\+/g, '/'); + const entries = util.normalizeFilesArray(files); + + function makeGlobAbsolute(pattern: string) { + return path.resolve( + path.posix.join( + rootDir, + //remove leading exclamation point if pattern is negated + pattern + //coerce all slashes to forward + ) + ).replace(/\\/g, '/'); + } + + let result: string; + + //add the file into every matching cache bucket + for (let entry of entries) { + const pattern = (typeof entry === 'string' ? entry : entry.src); + //filter previous paths + if (pattern.startsWith('!')) { + const keepFile = picomatch('!' + makeGlobAbsolute(pattern.replace(/^!/, ''))); + if (!keepFile(srcPathAbsolute)) { + result = undefined; + } + } else { + const keepFile = picomatch(makeGlobAbsolute(pattern)); + if (keepFile(srcPathAbsolute)) { + try { + result = this.computeFileDestPath( + srcPathAbsolute, + entry, + util.standardizePath(rootDir) + ); + } catch { + //ignore errors...the file just has no dest path + } + } + } + } + return result; + } + + /** + * Compute the `dest` path. This accounts for magic globstars in the pattern, + * as well as relative paths based on the dest. This is only used internally. + * @param src an absolute, normalized path for a file + * @param dest the `dest` entry for this file. If omitted, files will derive their paths relative to rootDir. + * @param pattern the glob pattern originally used to find this file + * @param rootDir absolute normalized path to the rootDir + */ + private computeFileDestPath(srcPath: string, entry: string | StandardizedFileEntry, rootDir: string) { + let result: string; + let globstarIdx: number; + //files under rootDir with no specified dest + if (typeof entry === 'string') { + if (util.isParentOfPath(rootDir, srcPath, false)) { + //files that are actually relative to rootDir + result = util.stringReplaceInsensitive(srcPath, rootDir, ''); + } else { + // result = util.stringReplaceInsensitive(srcPath, rootDir, ''); + throw new Error('Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); + } + + //non-glob-pattern explicit file reference + } else if (!isGlob(entry.src.replace(/\\/g, '/'), { strict: false })) { + let isEntrySrcAbsolute = path.isAbsolute(entry.src); + let entrySrcPathAbsolute = isEntrySrcAbsolute ? entry.src : util.standardizePath(`${rootDir}/${entry.src}`); + + let isSrcChildOfRootDir = util.isParentOfPath(rootDir, entrySrcPathAbsolute, false); + + let fileNameAndExtension = path.basename(entrySrcPathAbsolute); + + //no dest + if (entry.dest === null || entry.dest === undefined) { + //no dest, absolute path or file outside of rootDir + if (isEntrySrcAbsolute || isSrcChildOfRootDir === false) { + //copy file to root of staging folder + result = fileNameAndExtension; + + //no dest, relative path, lives INSIDE rootDir + } else { + //copy relative file structure to root of staging folder + let srcPathRelative = util.stringReplaceInsensitive(entrySrcPathAbsolute, rootDir, ''); + result = srcPathRelative; + } + + //assume entry.dest is the relative path to the folder AND file if applicable + } else if (entry.dest === '') { + result = fileNameAndExtension; + } else { + result = entry.dest; + } + //has a globstar + } else if ((globstarIdx = entry.src.indexOf('**')) > -1) { + const rootGlobstarPath = path.resolve(rootDir, entry.src.substring(0, globstarIdx)) + path.sep; + const srcPathRelative = util.stringReplaceInsensitive(srcPath, rootGlobstarPath, ''); + if (entry.dest) { + result = `${entry.dest}/${srcPathRelative}`; + } else { + result = srcPathRelative; + } + + //`pattern` is some other glob magic + } else { + const fileNameAndExtension = path.basename(srcPath); + if (entry.dest) { + result = util.standardizePath(`${entry.dest}/${fileNameAndExtension}`); + } else { + result = util.stringReplaceInsensitive(srcPath, rootDir, ''); + } + } + + result = util.standardizePath( + //remove leading slashes + result.replace(/^[\/\\]+/, '') + ); + return result; + } + + /** + * Given a root directory, normalize it to a full path. + * Fall back to cwd if not specified + * @param rootDir + */ + public normalizeRootDir(rootDir: string) { + if (!rootDir || (typeof rootDir === 'string' && rootDir.trim().length === 0)) { + return process.cwd(); + } else { + return path.resolve(rootDir); + } + } + } export let util = new Util(); From 52b1bc06153f67c059cb07a41ed6aa1a0f53ed8f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 12:56:47 -0500 Subject: [PATCH 14/63] Changed name of toTable --- src/commands/GetDeviceInfoCommand.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 33f01f5..9190cb5 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -1,10 +1,10 @@ -import { rokuDeploy, toTable } from '../index'; +import { rokuDeploy, printObjectToTable } from '../index'; export class GetDeviceInfoCommand { async run(args) { const outputPath = await rokuDeploy.getDeviceInfo({ host: args.host }); - console.log(toTable(outputPath)); + console.log(printObjectToTable(outputPath)); } } From f3a0c56f6cc37408923375f83eae1a07bcf5b83f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 13:31:48 -0500 Subject: [PATCH 15/63] Make test for objectToTable --- src/commands/GetDeviceInfoCommand.ts | 5 +++-- src/util.spec.ts | 20 ++++++++++++++++++++ src/util.ts | 28 ++++++++++++++-------------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 9190cb5..af45e53 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy, printObjectToTable } from '../index'; +import { rokuDeploy } from '../index'; +import { util } from '../util'; export class GetDeviceInfoCommand { async run(args) { const outputPath = await rokuDeploy.getDeviceInfo({ host: args.host }); - console.log(printObjectToTable(outputPath)); + console.log(util.printObjectToTable(outputPath)); } } diff --git a/src/util.spec.ts b/src/util.spec.ts index b174570..70f26e1 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -296,4 +296,24 @@ describe('util', () => { expect(util.decodeHtmlEntities(''')).to.eql(`'`); }); }); + + describe('printObjectToTable', () => { + it('should print an object to a table', () => { + const deviceInfo = { + 'device-id': '1234', + 'serial-number': 'abcd' + }; + + const result = util.printObjectToTable(deviceInfo); + + const expectedOutput = [ + 'Name Value ', + '---------------------------', + 'device-id 1234 ', + 'serial-number abcd ' + ].join('\n'); + + expect(result).to.eql(expectedOutput); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index a1e22c0..29e6c48 100644 --- a/src/util.ts +++ b/src/util.ts @@ -225,6 +225,20 @@ export class Util { }); } + public printObjectToTable(deviceInfo: Record) { + const margin = 5; + const keyWidth = Math.max(...Object.keys(deviceInfo).map(x => x.length)) + margin; + const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '')?.toString().length)) + margin; + let table = []; + table.push('Name'.padEnd(keyWidth, ' ') + 'Value'.padEnd(keyWidth, ' ')); + table.push('-'.repeat(keyWidth + valueWidth)); + for (const [key, value] of Object.entries(deviceInfo)) { + table.push(key.padEnd(keyWidth, ' ') + value?.toString().padEnd(keyWidth, ' ')); + } + + return table.join('\n'); + } + } export let util = new Util(); @@ -255,17 +269,3 @@ export function standardizePathPosix(stringParts, ...expressions: any[]) { result.join('') ); } - -export function printObjectToTable(deviceInfo: Record) { - const margin = 5; - const keyWidth = Math.max(...Object.keys(deviceInfo).map(x => x.length)) + margin; - const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '')?.toString().length)) + margin; - let table = []; - table.push('Name'.padEnd(keyWidth, ' ') + 'Value'.padEnd(keyWidth, ' ')); - table.push('-'.repeat(keyWidth + valueWidth)); - for (const [key, value] of Object.entries(deviceInfo)) { - table.push(key.padEnd(keyWidth, ' ') + value?.toString().padEnd(keyWidth, ' ')); - } - - return table.join('\n'); -} //TODO: Create a test for this function From b5f41ca07f388be604400913056400dc38d9148a Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 13:36:05 -0500 Subject: [PATCH 16/63] add yargs to pacjage.json --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f4f6ecb..14527d2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "parse-ms": "^2.1.0", "postman-request": "^2.88.1-postman.32", "temp-dir": "^2.0.0", - "xml2js": "^0.5.0" + "xml2js": "^0.5.0", + "yargs": "^17.7.2" }, "devDependencies": { "@types/chai": "^4.2.22", @@ -45,6 +46,7 @@ "@types/request": "^2.47.0", "@types/sinon": "^10.0.4", "@types/xml2js": "^0.4.5", + "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/parser": "5.1.0", "chai": "^4.3.4", From 4b959438a4d9d26ad8c60dd9a71a005c74e5bcbd Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 13:38:23 -0500 Subject: [PATCH 17/63] add package-lock.json --- package-lock.json | 199 +++++++++++++++++++++++++++++++--------------- 1 file changed, 137 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index febb616..3695c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "parse-ms": "^2.1.0", "postman-request": "^2.88.1-postman.32", "temp-dir": "^2.0.0", - "xml2js": "^0.5.0" + "xml2js": "^0.5.0", + "yargs": "^17.7.2" }, "bin": { "roku-deploy": "dist/cli.js" @@ -40,6 +41,7 @@ "@types/request": "^2.47.0", "@types/sinon": "^10.0.4", "@types/xml2js": "^0.4.5", + "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/parser": "5.1.0", "chai": "^4.3.4", @@ -804,6 +806,21 @@ "@types/node": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.1.0.tgz", @@ -1038,7 +1055,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1399,14 +1415,16 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/color-convert": { @@ -1675,8 +1693,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enquirer": { "version": "2.3.6", @@ -1700,7 +1717,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -2304,7 +2320,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2572,7 +2587,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -3212,6 +3226,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3278,6 +3303,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -4057,7 +4100,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4363,7 +4405,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4377,7 +4418,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4700,7 +4740,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4717,7 +4756,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4732,7 +4770,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4743,8 +4780,7 @@ "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrappy": { "version": "1.0.2", @@ -4788,7 +4824,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -4800,21 +4835,20 @@ "dev": true }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { @@ -4874,6 +4908,14 @@ "node": ">=8" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -5509,6 +5551,21 @@ "@types/node": "*" } }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.1.0.tgz", @@ -5647,8 +5704,7 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "3.2.1", @@ -5918,13 +5974,12 @@ "dev": true }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, @@ -6143,8 +6198,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enquirer": { "version": "2.3.6", @@ -6164,8 +6218,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "1.0.5", @@ -6605,8 +6658,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { "version": "2.0.0", @@ -6799,8 +6851,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.3", @@ -7291,6 +7342,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7335,6 +7397,21 @@ "requires": { "has-flag": "^4.0.0" } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } } } }, @@ -7932,8 +8009,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", @@ -8167,7 +8243,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8178,7 +8253,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -8410,7 +8484,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8421,7 +8494,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -8430,7 +8502,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -8438,8 +8509,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -8478,8 +8548,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "4.0.0", @@ -8488,18 +8557,24 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } } }, "yargs-parser": { From b6c6223017bd260fc788ae0e1277e848c44d6e80 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 13:40:05 -0500 Subject: [PATCH 18/63] add new line at eof --- src/commands/PublishCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/PublishCommand.ts b/src/commands/PublishCommand.ts index 91554fa..39a66cc 100644 --- a/src/commands/PublishCommand.ts +++ b/src/commands/PublishCommand.ts @@ -9,4 +9,4 @@ export class PublishCommand { outFile: args.outFile }); } -} \ No newline at end of file +} From f15f29543051421d6aa51ecc4f8978020bd05c9c Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 6 Feb 2024 13:51:56 -0500 Subject: [PATCH 19/63] Add test for coverage, also remove an unncessary '?' --- src/util.spec.ts | 18 ++++++++++++++++++ src/util.ts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/util.spec.ts b/src/util.spec.ts index 70f26e1..fd3d353 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -315,5 +315,23 @@ describe('util', () => { expect(result).to.eql(expectedOutput); }); + + it('should still print a table when a value is null', () => { + const deviceInfo = { + 'device-id': '1234', + 'serial-number': null + }; + + const result = util.printObjectToTable(deviceInfo); + + const expectedOutput = [ + 'Name Value ', + '---------------------------', + 'device-id 1234 ', + 'serial-number undefined' + ].join('\n'); + + expect(result).to.eql(expectedOutput); + }); }); }); diff --git a/src/util.ts b/src/util.ts index 29e6c48..c275c88 100644 --- a/src/util.ts +++ b/src/util.ts @@ -228,7 +228,7 @@ export class Util { public printObjectToTable(deviceInfo: Record) { const margin = 5; const keyWidth = Math.max(...Object.keys(deviceInfo).map(x => x.length)) + margin; - const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '')?.toString().length)) + margin; + const valueWidth = Math.max(...Object.values(deviceInfo).map(x => (x ?? '').toString().length)) + margin; let table = []; table.push('Name'.padEnd(keyWidth, ' ') + 'Value'.padEnd(keyWidth, ' ')); table.push('-'.repeat(keyWidth + valueWidth)); From 6941e86aad2a1ae4bb4733d931b72421db52754a Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 6 Feb 2024 15:17:09 -0500 Subject: [PATCH 20/63] tweaks --- src/cli.ts | 36 ++++++++++++++++++++---------------- src/commands/ExecCommand.ts | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 src/commands/ExecCommand.ts diff --git a/src/cli.ts b/src/cli.ts index d54fcbc..d601f12 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -38,28 +38,32 @@ void yargs .command('devid') // alias: getDevId //bundle - .command('stage|zip') + .command('bundle', ()=>{ + const command = new ExecCommand({ + actions: 'stage|zip' + }) + }) //deploy - .command('stage|zip|delete|close|sideload') + .command('deploy', ()=>{ + const command = new ExecCommand({ + actions: 'stage|zip|delete|close|sideload' + }) + }) //package - .command('close|rekey|stage|zip|delete|close|sideload|squash|sign') + .command('package', ()=>{ + const command = new ExecCommand({ + actions: 'close|rekey|stage|zip|delete|close|sideload|squash|sign' + }) + }) //exec - .command('magic') - - - - - - - - - - - - + .command('exec', ()=>{ + const command = new ExecCommand({ + actions: builder.args.actions + }) + }) diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts new file mode 100644 index 0000000..73dfd64 --- /dev/null +++ b/src/commands/ExecCommand.ts @@ -0,0 +1,15 @@ +export class ExecCommand { + constructor({actions: string }){ + this.actions = options.actions.split('|'); + } + + run() { + if(this.actions.includes('stage')){ + rokuDeploy.stage(); + } + + if(this.actions.includes('zip')){ + rokuDeploy.zip(); + } + } +} \ No newline at end of file From ea493425a9579a3746a37105e80d8d245e5e9248 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 6 Feb 2024 15:17:28 -0500 Subject: [PATCH 21/63] tweaks From 616ff23dc97ee532ccda616f109bc15d40eebae6 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 6 Feb 2024 15:24:19 -0500 Subject: [PATCH 22/63] get from json stuff --- src/commands/ExecCommand.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index 73dfd64..a0405c7 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -1,9 +1,15 @@ export class ExecCommand { - constructor({actions: string }){ + constructor(options: {actions: string, configPath: string, ...rokuDeployOptions }){ this.actions = options.actions.split('|'); } run() { + //load options from json + const options = { + ...getFromJson(this.configPath ?? `${cwd}/rokudeploy.json`), + this.options + }; + if(this.actions.includes('stage')){ rokuDeploy.stage(); } From cacf4337e997ae1814904578212559c6637c2150 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 7 Feb 2024 12:45:37 -0500 Subject: [PATCH 23/63] Correct imports --- src/RokuDeploy.ts | 27 ++++++++++++--------------- src/util.ts | 4 ++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 28e1059..7401fa6 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1,20 +1,17 @@ import * as path from 'path'; -import * as _fsExtra from 'fs-extra'; +import * as fsExtra from 'fs-extra'; //TODO: This was removed in Bronley commit. How to fix errors? +import type { WriteStream, ReadStream } from 'fs-extra'; import * as r from 'postman-request'; import type * as requestType from 'request'; const request = r as typeof requestType; import * as JSZip from 'jszip'; -import * as dateformat from 'dateformat'; import * as errors from './Errors'; -import * as isGlob from 'is-glob'; -import * as picomatch from 'picomatch'; import * as xml2js from 'xml2js'; import type { ParseError } from 'jsonc-parser'; import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; import { util } from './util'; import type { RokuDeployOptions, FileEntry } from './RokuDeployOptions'; import { Logger, LogLevel } from './Logger'; -import * as tempDir from 'temp-dir'; import * as dayjs from 'dayjs'; import * as lodash from 'lodash'; import type { DeviceInfo, DeviceInfoRaw } from './DeviceInfo'; @@ -224,7 +221,7 @@ export class RokuDeploy { } } - let readStream: _fsExtra.ReadStream; + let readStream: ReadStream; try { if ((await this.fsExtra.pathExists(zipFilePath)) === false) { throw new Error(`Cannot publish because file does not exist at '${zipFilePath}'`); @@ -337,12 +334,12 @@ export class RokuDeploy { let requestOptions = this.generateBaseRequestOptions('plugin_inspect', options as any, { mysubmit: 'Rekey', passwd: options.signingPassword, - archive: null as _fsExtra.ReadStream + archive: null as ReadStream }); let results: HttpResponse; try { - requestOptions.formData.archive = this.fsExtra.createReadStream(rekeySignedPackagePath); + requestOptions.formData.archive = fsExtra.createReadStream(rekeySignedPackagePath); results = await this.doPostRequest(requestOptions); } finally { //ensure the stream is closed @@ -603,10 +600,10 @@ export class RokuDeploy { } private async getToFile(requestParams: any, filePath: string) { - let writeStream: _fsExtra.WriteStream; - await this.fsExtra.ensureFile(filePath); + let writeStream: WriteStream; + await fsExtra.ensureFile(filePath); return new Promise((resolve, reject) => { - writeStream = this.fsExtra.createWriteStream(filePath, { + writeStream = fsExtra.createWriteStream(filePath, { flags: 'w' }); if (!writeStream) { @@ -649,8 +646,8 @@ export class RokuDeploy { } for (const fileName of fileNames) { - if (this.fsExtra.existsSync(fileName)) { - let configFileText = this.fsExtra.readFileSync(fileName).toString(); + if (fsExtra.existsSync(fileName)) { + let configFileText = fsExtra.readFileSync(fileName).toString(); let parseErrors = [] as ParseError[]; fileOptions = parseJsonc(configFileText, parseErrors, { allowEmptyContent: true, @@ -831,11 +828,11 @@ export class RokuDeploy { * TODO move these manifest functions to a util somewhere */ private async parseManifest(manifestPath: string): Promise { - if (!await this.fsExtra.pathExists(manifestPath)) { + if (!await fsExtra.pathExists(manifestPath)) { throw new Error(manifestPath + ' does not exist'); } - let manifestContents = await this.fsExtra.readFile(manifestPath, 'utf-8'); + let manifestContents = await fsExtra.readFile(manifestPath, 'utf-8'); return this.parseManifestFromString(manifestContents); } diff --git a/src/util.ts b/src/util.ts index f0a25a2..c1d648f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,6 +5,10 @@ import * as dns from 'dns'; import * as micromatch from 'micromatch'; // eslint-disable-next-line @typescript-eslint/no-require-imports import fastGlob = require('fast-glob'); +import type { FileEntry } from './RokuDeployOptions'; +import type { StandardizedFileEntry } from './RokuDeploy'; +import * as isGlob from 'is-glob'; +import * as picomatch from 'picomatch'; export class Util { /** From cbd641779bf10ad16fd2cb7c605b85205a0fa6a0 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 7 Feb 2024 13:02:25 -0500 Subject: [PATCH 24/63] Last of the fsExtra changes --- src/RokuDeploy.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 7401fa6..0105fde 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -54,12 +54,12 @@ export class RokuDeploy { let destFilePath = util.standardizePath(`${stagingPath}/${fileObject.dest}`); //make sure the containing folder exists - await this.fsExtra.ensureDir(path.dirname(destFilePath)); + await fsExtra.ensureDir(path.dirname(destFilePath)); //sometimes the copyfile action fails due to race conditions (normally to poorly constructed src;dest; objects with duplicate files in them await util.tryRepeatAsync(async () => { //copy the src item using the filesystem - await this.fsExtra.copy(fileObject.src, destFilePath, { + await fsExtra.copy(fileObject.src, destFilePath, { //copy the actual files that symlinks point to, not the symlinks themselves dereference: true }); @@ -76,7 +76,7 @@ export class RokuDeploy { options = this.getOptions(options) as any; //make sure the output folder exists - await this.fsExtra.ensureDir(options.outDir); + await fsExtra.ensureDir(options.outDir); let zipFilePath = this.getOutputZipFilePath(options as any); @@ -103,7 +103,7 @@ export class RokuDeploy { // Allows us to wait until all are done before we build the zip const promises = []; for (const file of filePaths) { - const promise = this.fsExtra.readFile(file.src).then((data) => { + const promise = fsExtra.readFile(file.src).then((data) => { if (preFileZipCallback) { data = preFileZipCallback(file, data); } @@ -123,7 +123,7 @@ export class RokuDeploy { await Promise.all(promises); // level 2 compression seems to be the best balance between speed and file size. Speed matters more since most will be calling squashfs afterwards. const content = await zip.generateAsync({ type: 'nodebuffer', compressionOptions: { level: 2 } }); - return this.fsExtra.writeFile(zipFilePath, content); + return fsExtra.writeFile(zipFilePath, content); } private generateBaseRequestOptions(requestPath: string, options: BaseRequestOptions, formData = {} as T): requestType.OptionsWithUrl { @@ -209,7 +209,7 @@ export class RokuDeploy { throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); } //make sure the outDir exists - await this.fsExtra.ensureDir(options.outDir); + await fsExtra.ensureDir(options.outDir); let zipFilePath = this.getOutputZipFilePath(options as any); @@ -223,10 +223,10 @@ export class RokuDeploy { let readStream: ReadStream; try { - if ((await this.fsExtra.pathExists(zipFilePath)) === false) { + if ((await fsExtra.pathExists(zipFilePath)) === false) { throw new Error(`Cannot publish because file does not exist at '${zipFilePath}'`); } - readStream = this.fsExtra.createReadStream(zipFilePath); + readStream = fsExtra.createReadStream(zipFilePath); //wait for the stream to open (no harm in doing this, and it helps solve an issue in the tests) await new Promise((resolve) => { readStream.on('open', resolve); @@ -275,7 +275,7 @@ export class RokuDeploy { } finally { //delete the zip file only if configured to do so if (options.retainDeploymentArchive === false) { - await this.fsExtra.remove(zipFilePath); + await fsExtra.remove(zipFilePath); } //try to close the read stream to prevent files becoming locked try { From ebab568eb8a5dd3d50e837f9cd79a6869a40e29d Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 21 Feb 2024 14:19:59 -0500 Subject: [PATCH 25/63] Create a load from json util, clean up cli.ts file with fleshing out comments, delete unnecessary command/tests/options, partially fix exec command --- src/RokuDeploy.spec.ts | 176 --------------------------- src/RokuDeploy.ts | 39 +----- src/cli.ts | 122 ++++++++++--------- src/commands/CreatePackageCommand.ts | 11 -- src/commands/ExecCommand.ts | 71 +++++++++-- src/commands/TextCommand.ts | 8 ++ src/util.ts | 19 +++ 7 files changed, 153 insertions(+), 293 deletions(-) delete mode 100644 src/commands/CreatePackageCommand.ts create mode 100644 src/commands/TextCommand.ts diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 53e76b2..93da38c 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -703,182 +703,6 @@ describe('index', () => { }); - describe('createPackage', () => { - it('works with custom stagingDir', async () => { - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: '.tmp/dist', - outDir: outDir, - rootDir: rootDir - }); - expectPathExists(rokuDeploy.getOutputZipFilePath({ outDir: outDir })); - }); - - it('should throw error when no files were found to copy', async () => { - await assertThrowsAsync(async () => { - await rokuDeploy.createPackage({ - files: [], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }); - }); - }); - - it('should create package in proper directory', async () => { - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }); - expectPathExists(rokuDeploy.getOutputZipFilePath({ outDir: outDir })); - }); - - it('should only include the specified files', async () => { - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }); - const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath({ outDir: outDir })); - const zip = await JSZip.loadAsync(data); - - const files = ['manifest']; - for (const file of files) { - const zipFileContents = await zip.file(file.toString()).async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - expect(zipFileContents).to.equal(incomingContents); - } - }); - - it('generates full package with defaults', async () => { - const filePaths = writeFiles(rootDir, [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]); - await rokuDeploy.createPackage({ - files: filePaths, - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }); - - const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath({ outDir: outDir })); - const zip = await JSZip.loadAsync(data); - - for (const file of filePaths) { - const zipFileContents = await zip.file(file.toString())?.async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - expect(zipFileContents).to.equal(incomingContents); - } - }); - - it('should retain the staging directory when told to', async () => { - let stagingDirValue = await rokuDeploy.stage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - rootDir: rootDir - }); - expectPathExists(stagingDirValue); - await rokuDeploy.zip({ - stagingDir: stagingDir, - retainStagingDir: true, - outDir: outDir - }); - expectPathExists(stagingDirValue); - }); - - it('should call our callback with correct information', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, 'major_version=1'); - - let spy = sinon.spy((info: BeforeZipCallbackInfo) => { - expectPathExists(info.stagingDir); - expect(info.manifestData.major_version).to.equal('1'); - }); - - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }, spy); - - if (spy.notCalled) { - assert.fail('Callback not called'); - } - }); - - it('should wait for promise returned by pre-zip callback', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, ''); - let count = 0; - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir - }, (info) => { - return Promise.resolve().then(() => { - count++; - }).then(() => { - count++; - }); - }); - expect(count).to.equal(2); - }); - - it('should increment the build number if requested', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, `build_version=0`); - //make the zipping immediately resolve - sinon.stub(rokuDeploy, 'zipPackage').returns(Promise.resolve()); - let beforeZipInfo: BeforeZipCallbackInfo; - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir, - incrementBuildNumber: true - }, (info) => { - beforeZipInfo = info; - }); - expect(beforeZipInfo.manifestData.build_version).to.not.equal('0'); - }); - - it('should not increment the build number if not requested', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, `build_version=0`); - await rokuDeploy.createPackage({ - files: [ - 'manifest' - ], - stagingDir: stagingDir, - outDir: outDir, - rootDir: rootDir, - incrementBuildNumber: false - }, (info) => { - expect(info.manifestData.build_version).to.equal('0'); - }); - }); - }); - it('runs via the command line using the rokudeploy.json file', function test() { this.timeout(20000); //build the project diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 0105fde..bb3f959 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import * as fsExtra from 'fs-extra'; //TODO: This was removed in Bronley commit. How to fix errors? +import * as fsExtra from 'fs-extra'; import type { WriteStream, ReadStream } from 'fs-extra'; import * as r from 'postman-request'; import type * as requestType from 'request'; @@ -640,34 +640,6 @@ export class RokuDeploy { */ private __getOptions(options: RokuDeployOptions = {}) { let fileOptions: RokuDeployOptions = {}; - const fileNames = ['rokudeploy.json', 'bsconfig.json']; - if (options.project) { - fileNames.unshift(options.project); - } - - for (const fileName of fileNames) { - if (fsExtra.existsSync(fileName)) { - let configFileText = fsExtra.readFileSync(fileName).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(fileName)}": ` + JSON.stringify( - parseErrors.map(x => { - return { - message: printParseErrorCode(x.error), - offset: x.offset, - length: x.length - }; - }) - )); - } - break; - } - } let defaultOptions = { outDir: './out', @@ -997,15 +969,6 @@ export interface ZipPackageOptions { outDir?: string; } -export interface CreatePackageOptions { - rootDir?: string; - files?: FileEntry[]; - stagingDir?: string; - retainStagingDir?: boolean; - outDir?: string; - incrementBuildNumber?: boolean; -} - export interface PublishOptions { host: string; password: string; diff --git a/src/cli.ts b/src/cli.ts index 72da67b..8be0fba 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,10 @@ #!/usr/bin/env node import * as yargs from 'yargs'; +import { rokuDeploy } from './index'; +import { ExecCommand } from './commands/ExecCommand'; +import { TextCommand } from './commands/TextCommand'; import { PrepublishCommand } from './commands/PrepublishCommand'; import { ZipPackageCommand } from './commands/ZipPackageCommand'; -import { CreatePackageCommand } from './commands/CreatePackageCommand'; import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; @@ -19,52 +21,71 @@ import { ZipFolderCommand } from './commands/ZipFolderCommand'; void yargs - //not exposed - // .command('stage') - // .command('zip') - // .command('closeChannel') - // .command('sideload') - // .command('convertToSquashfs') //alias: squash - // .command('rekeyDevice') //alias: rekey - // .command('createSignedPackage') //alias: sign - // .command('deleteDevChannel') // alias: rmdev deldev + .command('bundle', 'execute build actions for bundling app', (builder) => { + return builder + .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + }, (args: any) => { + return new ExecCommand( + 'stage|zip', + args.configPath + ).run(); + }) + + .command('deploy', 'execute build actions for deploying app', (builder) => { + return builder + .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + }, (args: any) => { + return new ExecCommand( + 'stage|zip|delete|close|sideload', + args.configPath + ).run(); + }) - .command('keypress') - .command('keyup') - .command('keydown') - .command('text') //alias: sendText - .command('screenshot') // alias: captureScreenshot - .command('deviceinfo') // alias: getDeviceInfo - .command('devid') // alias: getDevId + .command('package', 'execute build actions for packaging app', (builder) => { + return builder + .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + }, (args: any) => { + return new ExecCommand( + 'close|rekey|stage|zip|delete|close|sideload|squash|sign', + args.configPath + ).run(); + }) - //bundle - .command('bundle', ()=>{ - const command = new ExecCommand({ - actions: 'stage|zip' - }) + .command('exec', 'larger command for handling a series of smaller commands', (builder) => { + return builder + .option('actions', { type: 'string', description: 'The actions to be executed, separated by |', demandOption: true }) + .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + }, (args: any) => { + return new ExecCommand(args.actions, args.configPath).run(); }) - //deploy - .command('deploy', ()=>{ - const command = new ExecCommand({ - actions: 'stage|zip|delete|close|sideload' - }) + .command(['sendText', 'text'], 'Send text command', (builder) => { + return builder + .option('text', { type: 'string', description: 'The text to send', demandOption: true }); + }, (args: any) => { + return new TextCommand().run(args); //TODO: do this through exec command to get default args like host and port? or add those to here and go through separate command for better testing }) - //package - .command('package', ()=>{ - const command = new ExecCommand({ - actions: 'close|rekey|stage|zip|delete|close|sideload|squash|sign' - }) + .command('keypress', 'send keypress command', (builder) => { + return builder + .option('key', { type: 'string', description: 'The key to send', demandOption: true }); + }, async (args: any) => { + await rokuDeploy.keyPress(args.text); //TODO: Go through exec command, separate command, or use key event? }) - //exec - .command('exec', ()=>{ - const command = new ExecCommand({ - actions: builder.args.actions - }) + .command('keyup', 'send keyup command', (builder) => { + return builder + .option('key', { type: 'string', description: 'The key to send', demandOption: true }); + }, async (args: any) => { + await rokuDeploy.keyUp(args.text); //TODO: Go through exec command, separate command, or use key event? }) + .command('keydown', 'send keydown command', (builder) => { + return builder + .option('key', { type: 'string', description: 'The key to send', demandOption: true }); + }, async (args: any) => { + await rokuDeploy.keyDown(args.text); //TODO: Go through exec command, separate command, or use key event? + }) .command(['stage', 'prepublishToStaging'], 'Copies all of the referenced files to the staging folder', (builder) => { return builder @@ -74,7 +95,7 @@ void yargs return new PrepublishCommand().run(args); }) - .command('zip', 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { + .command(['zip', 'zipPackage'], 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { return builder .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); @@ -82,15 +103,6 @@ void yargs return new ZipPackageCommand().run(args); }) - .command('createPackage', 'Create a zip folder containing all of the specified roku project files', (builder) => { - return builder - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) - .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); - }, (args: any) => { - return new CreatePackageCommand().run(args); - }) - .command('publish', 'Publish a pre-existing packaged zip file to a remote Roku', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) @@ -101,7 +113,7 @@ void yargs return new PublishCommand().run(args); }) - .command('convertToSquashfs', 'Convert a pre-existing packaged zip file to a squashfs file', (builder) => { + .command(['squash', 'convertToSquashfs'], 'Convert a pre-existing packaged zip file to a squashfs file', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); @@ -109,7 +121,7 @@ void yargs return new ConvertToSquashfsCommand().run(args); }) - .command('rekeyDevice', 'Rekey a device', (builder) => { + .command(['rekey', 'rekeyDevice'], 'Rekey a device', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) @@ -121,7 +133,7 @@ void yargs return new RekeyDeviceCommand().run(args); }) - .command('signExistingPackage', 'Sign a package', (builder) => { + .command('createSignedPackage', 'Sign a package', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) @@ -149,7 +161,7 @@ void yargs return new DeployCommand().run(args); }) - .command('deleteInstalledChannel', 'Delete an installed channel', (builder) => { + .command(['deleteDevChannel', 'deleteInstalledChannel', 'rmdev', 'delete'], 'Delete an installed channel', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); @@ -157,12 +169,12 @@ void yargs return new DeleteInstalledChannelCommand().run(args); }) - .command('takeScreenshot', 'Take a screenshot', (builder) => { + .command(['screenshot', 'captureScreenshot'], 'Take a screenshot', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); }, (args: any) => { - return new TakeScreenshotCommand().run(args); + return new TakeScreenshotCommand().run(args);//TODO: rename }) .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { @@ -182,14 +194,14 @@ void yargs return new GetOutputPkgFilePathCommand().run(args); }) - .command('getDeviceInfo', 'Get the `device-info` response from a Roku device', (builder) => { + .command(['getDeviceInfo', 'deviceinfo'], 'Get the `device-info` response from a Roku device', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }); }, (args: any) => { return new GetDeviceInfoCommand().run(args); }) - .command('getDevId', 'Get Dev ID', (builder) => { + .command(['getDevId', 'devid'], 'Get Dev ID', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }); }, (args: any) => { diff --git a/src/commands/CreatePackageCommand.ts b/src/commands/CreatePackageCommand.ts deleted file mode 100644 index f4b4718..0000000 --- a/src/commands/CreatePackageCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { rokuDeploy } from '../index'; - -export class CreatePackageCommand { - async run(args) { - await rokuDeploy.createPackage({ - stagingDir: args.stagingDir, - outDir: args.outDir, - rootDir: args.rootDir - }); - } -} diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index a0405c7..bf4cfc3 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -1,21 +1,66 @@ +import { rokuDeploy } from '../index'; +import { cwd } from '../testUtils.spec'; +import { util } from '../util'; + export class ExecCommand { - constructor(options: {actions: string, configPath: string, ...rokuDeployOptions }){ - this.actions = options.actions.split('|'); + private actions: string[]; + + private configPath: string; + + // eslint-disable-next-line @typescript-eslint/ban-types + private options: {}; + + constructor(actions: string, configPath: string, ...rokuDeployOptions) { + this.actions = actions.split('|'); + this.configPath = configPath; + this.options = rokuDeployOptions; } - run() { + async run() { //load options from json - const options = { - ...getFromJson(this.configPath ?? `${cwd}/rokudeploy.json`), - this.options - }; - - if(this.actions.includes('stage')){ - rokuDeploy.stage(); + this.options = util.getOptionsFromJson(this.options); + + // Possibilities: + // 'stage|zip' + // 'stage|zip|delete|close|sideload' + // 'close|rekey|stage|zip|delete|close|sideload|squash|sign' + + if (this.actions.includes('stage')) { + await rokuDeploy.stage(this.options); + } + + if (this.actions.includes('zip')) { + await rokuDeploy.zip(this.options); + } + + if (this.actions.includes('delete')) { + await rokuDeploy.deleteDevChannel(this.options); + } + + if (this.actions.includes('close')) { + await rokuDeploy.closeChannel(this.options); + } + + if (this.actions.includes('sideload')) { + await rokuDeploy.sideload(this.options); } - if(this.actions.includes('zip')){ - rokuDeploy.zip(); + if (this.actions.includes('stage')) { + await rokuDeploy.stage(this.options); } + + if (this.actions.includes('rekey')) { + await rokuDeploy.rekeyDevice(this.options); + } + + if (this.actions.includes('squash')) { + await rokuDeploy.convertToSquashfs(this.options); + } + + if (this.actions.includes('sign')) { + await rokuDeploy.createSignedPackage(this.options); + } + + } -} \ No newline at end of file +} diff --git a/src/commands/TextCommand.ts b/src/commands/TextCommand.ts new file mode 100644 index 0000000..5408ef8 --- /dev/null +++ b/src/commands/TextCommand.ts @@ -0,0 +1,8 @@ +import { rokuDeploy } from '../index'; + +export class TextCommand { + // this.options = getDefaultArgsFromJson(this.configPath ?? `${cwd}/rokudeploy.json`);TODO + async run(args) { + await rokuDeploy.sendText(args.text); + } +} diff --git a/src/util.ts b/src/util.ts index c1d648f..e807ff4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -483,6 +483,25 @@ export class Util { return table.join('\n'); } + /** + * A function to fill in any missing arguments with JSON values + */ + public getOptionsFromJson(defaultArgs) { //TODO: create a test for this in cli.spec.ts too + let args = { ...defaultArgs }; + const fileNames = ['rokudeploy.json', 'bsconfig.json']; + + for (const fileName of fileNames) { + if (fsExtra.existsSync(fileName)) { + let rokudeployArgs = JSON.parse(fs.readFileSync('rokudeploy.json', 'utf-8')); + // args = Object.assign(rokudeployArgs ?? {}, args); + args = Object.assign(rokudeployArgs, args); + break; + } + } + + return args; + } + } export let util = new Util(); From 46fe1293f81317105fb6e1d70199116195a06a5c Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 22 Feb 2024 14:32:37 -0500 Subject: [PATCH 26/63] Adding cli tests --- src/cli.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 4cf25f3..71d486b 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -39,6 +39,11 @@ describe('cli', () => { sinon.restore(); }); + it('Successfully bundles an app', () => { + execSync(`node ${cwd}/dist/cli.js bundle --rootDir ${rootDir} --outDir ${outDir}`); + expectPathExists(`${outDir}/roku-deploy.zip`); + }); + it('Successfully runs prepublishToStaging', () => { //make the files fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); @@ -63,11 +68,6 @@ describe('cli', () => { expectPathExists(`${outDir}/roku-deploy.zip`); }); - it('Successfully uses createPackage to create .pkg', () => { - execSync(`node ${cwd}/dist/cli.js createPackage --stagingDir ${stagingDir} --rootDir ${rootDir} --outDir ${outDir}`); - expectPathExists(`${outDir}/roku-deploy.zip`); - }); - it('Publish passes proper options', async () => { const stub = sinon.stub(rokuDeploy, 'publish').callsFake(async () => { return Promise.resolve({ From 36502e18e849be5e25a71bb7cc8194b41082c3e2 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 22 Feb 2024 14:33:12 -0500 Subject: [PATCH 27/63] Delete deployAndSignPackage tests, a deleted function in rokudeploy --- src/RokuDeploy.spec.ts | 89 ------------------------------------------ 1 file changed, 89 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 93da38c..c9447b7 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -3597,95 +3597,6 @@ describe('index', () => { expect(getToFileIsResolved).to.be.true; }); }); - - describe('deployAndSignPackage', () => { - beforeEach(() => { - //pretend the deploy worked - sinon.stub(rokuDeploy, 'deploy').returns(Promise.resolve(null)); - //pretend the sign worked - sinon.stub(rokuDeploy, 'signExistingPackage').returns(Promise.resolve(null)); - //pretend fetching the signed package worked - sinon.stub(rokuDeploy, 'retrieveSignedPackage').returns(Promise.resolve('some_local_path')); - }); - - it('succeeds and does proper things with staging folder', async () => { - let stub = sinon.stub(rokuDeploy['fsExtra'], 'remove').returns(Promise.resolve() as any); - - //this should not fail - let pkgFilePath = await rokuDeploy.deployAndSignPackage({ - host: '1.2.3.4', - password: 'password', - signingPassword: 'secret', - retainStagingDir: false - }); - - //the return value should equal what retrieveSignedPackage returned. - expect(pkgFilePath).to.equal('some_local_path'); - - //fsExtra.remove should have been called - expect(stub.getCalls()).to.be.lengthOf(1); - - //call it again, but specify true for retainStagingDir - await rokuDeploy.deployAndSignPackage({ - host: '1.2.3.4', - password: 'password', - signingPassword: 'secret', - retainStagingDir: true - }); - //call count should NOT increase - expect(stub.getCalls()).to.be.lengthOf(1); - - //call it again, but don't specify retainStagingDir at all (it should default to FALSE) - await rokuDeploy.deployAndSignPackage({ - host: '1.2.3.4', - password: 'password', - signingPassword: 'secret' - }); - //call count should NOT increase - expect(stub.getCalls()).to.be.lengthOf(2); - }); - - it('converts to squashfs if we request it to', async () => { - // options.convertToSquashfs = true; - let stub = sinon.stub(rokuDeploy, 'convertToSquashfs').returns(Promise.resolve(null)); - await rokuDeploy.deployAndSignPackage({ - host: '1.2.3.4', - password: 'password', - signingPassword: 'secret', - convertToSquashfs: true - }); - expect(stub.getCalls()).to.be.lengthOf(1); - }); - }); - - function mockDoGetRequest(body = '', statusCode = 200) { - return sinon.stub(rokuDeploy as any, 'doGetRequest').callsFake((params) => { - let results = { response: { statusCode: statusCode }, body: body }; - rokuDeploy['checkRequest'](results); - return Promise.resolve(results); - }); - } - - function mockDoPostRequest(body = '', statusCode = 200) { - return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { - let results = { response: { statusCode: statusCode }, body: body }; - rokuDeploy['checkRequest'](results); - return Promise.resolve(results); - }); - } - - async function assertThrowsAsync(fn) { - let f = () => { }; - try { - await fn(); - } catch (e) { - f = () => { - throw e; - }; - } finally { - assert.throws(f); - } - } }); function getFakeResponseBody(messages: string): string { From c058dd6455ffe3c23f9c82197d59c23c01593861 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 22 Feb 2024 14:33:57 -0500 Subject: [PATCH 28/63] Add defaultsFromJson tests --- src/util.spec.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++- src/util.ts | 5 +++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/util.spec.ts b/src/util.spec.ts index f58c18c..eeb4726 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -1,7 +1,7 @@ import { util, standardizePath as s } from './util'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; -import { tempDir } from './testUtils.spec'; +import { tempDir, rootDir } from './testUtils.spec'; import * as path from 'path'; import * as dns from 'dns'; import { createSandbox } from 'sinon'; @@ -14,6 +14,7 @@ describe('util', () => { }); afterEach(() => { + fsExtra.emptyDirSync(tempDir); sinon.restore(); }); @@ -334,4 +335,53 @@ describe('util', () => { expect(result).to.eql(expectedOutput); }); }); + + describe('getOptionsFromJson', () => { + beforeEach(() => { + fsExtra.ensureDirSync(rootDir); + process.chdir(rootDir); + }); + it('should fill in missing options from rokudeploy.json', () => { + fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); + let options = util.getOptionsFromJson({ + rootDir: `${rootDir}`, + host: '1.2.3.4' + }); + let expectedOutput = { + rootDir: `${rootDir}`, + host: '1.2.3.4', + password: 'password' + }; + expect(options).to.eql(expectedOutput); + }); + + it('should fill in missing default options from bsconfig.json', () => { + fsExtra.writeJsonSync(s`${rootDir}/bsconfig.json`, { password: 'password' }); + let options = util.getOptionsFromJson({ + rootDir: `${rootDir}`, + host: '1.2.3.4' + }); + let expectedOutput = { + rootDir: `${rootDir}`, + host: '1.2.3.4', + password: 'password' + }; + expect(options).to.eql(expectedOutput); + + }); + + it('should not replace default options', () => { + fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { host: '4.3.2.1' }); + let options = util.getOptionsFromJson({ + rootDir: `${rootDir}`, + host: '1.2.3.4' + }); + let expectedOutput = { + rootDir: `${rootDir}`, + host: '1.2.3.4', + }; + expect(options).to.eql(expectedOutput); + + }); + }); }); diff --git a/src/util.ts b/src/util.ts index e807ff4..9fd883b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -492,9 +492,10 @@ export class Util { for (const fileName of fileNames) { if (fsExtra.existsSync(fileName)) { - let rokudeployArgs = JSON.parse(fs.readFileSync('rokudeploy.json', 'utf-8')); + const sourcePath = path.join(defaultArgs.rootDir, fileName); + const rokuDeployArgs = JSON.parse(fsExtra.readFileSync(sourcePath, 'utf8')); // args = Object.assign(rokudeployArgs ?? {}, args); - args = Object.assign(rokudeployArgs, args); + args = Object.assign(rokuDeployArgs, args); break; } } From 5859f91d78d8103e77409f0aac94c259a63d9291 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 08:58:09 -0500 Subject: [PATCH 29/63] Move some tests to utils test file, re add other functions that might have been accidentally deleted, fix some errors with fsExtra --- src/RokuDeploy.spec.ts | 168 ++++++++++------------------------------- src/util.spec.ts | 117 +++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 128 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index c9447b7..99056a3 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -631,12 +631,12 @@ describe('index', () => { it('computes absolute path for all operations', async () => { const ensureDirPaths = []; - sinon.stub(rokuDeploy.fsExtra, 'ensureDir').callsFake((p) => { + sinon.stub(fsExtra, 'ensureDir').callsFake((p) => { ensureDirPaths.push(p); return Promise.resolve; }); const copyPaths = [] as Array<{ src: string; dest: string }>; - sinon.stub(rokuDeploy.fsExtra as any, 'copy').callsFake((src, dest) => { + sinon.stub(fsExtra as any, 'copy').callsFake((src, dest) => { copyPaths.push({ src: src as string, dest: dest as string }); return Promise.resolve(); }); @@ -828,10 +828,10 @@ describe('index', () => { }); it('failure to close read stream does not crash', async () => { - const orig = rokuDeploy.fsExtra.createReadStream; + const orig = fsExtra.createReadStream; //wrap the stream.close call so we can throw - sinon.stub(rokuDeploy.fsExtra, 'createReadStream').callsFake((pathLike) => { - const stream = orig.call(rokuDeploy.fsExtra, pathLike); + sinon.stub(fsExtra, 'createReadStream').callsFake((pathLike) => { + const stream = orig.call(fsExtra, pathLike); const originalClose = stream.close; stream.close = () => { originalClose.call(stream); @@ -1810,17 +1810,17 @@ describe('index', () => { }); }); it('is resilient to file system errors', async () => { - let copy = rokuDeploy.fsExtra.copy; + let copy = fsExtra.copy; let count = 0; //mock writeFile so we can throw a few errors during the test - sinon.stub(rokuDeploy.fsExtra, 'copy').callsFake((...args) => { + sinon.stub(fsExtra, 'copy').callsFake((...args) => { count += 1; //fail a few times if (count < 5) { throw new Error('fake error thrown as part of the unit test'); } else { - return copy.apply(rokuDeploy.fsExtra, args); + return copy.apply(fsExtra, args); } }); @@ -1844,17 +1844,17 @@ describe('index', () => { }); it('throws underlying error after the max fs error threshold is reached', async () => { - let copy = rokuDeploy.fsExtra.copy; + let copy = fsExtra.copy; let count = 0; //mock writeFile so we can throw a few errors during the test - sinon.stub(rokuDeploy.fsExtra, 'copy').callsFake((...args) => { + sinon.stub(fsExtra, 'copy').callsFake((...args) => { count += 1; //fail a few times if (count < 15) { throw new Error('fake error thrown as part of the unit test'); } else { - return copy.apply(rokuDeploy.fsExtra, args); + return copy.apply(fsExtra, args); } }); @@ -3127,125 +3127,10 @@ describe('index', () => { }); }); - describe('getDestPath', () => { - it('handles unrelated exclusions properly', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/components/comp1/comp1.brs`, - [ - '**/*', - '!exclude.me' - ], - rootDir - ) - ).to.equal(s`components/comp1/comp1.brs`); - }); - - it('finds dest path for top-level path', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/components/comp1/comp1.brs`, - ['components/**/*'], - rootDir - ) - ).to.equal(s`components/comp1/comp1.brs`); - }); - - it('does not find dest path for non-matched top-level path', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - ['components/**/*'], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes a file that is negated', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - [ - 'source/**/*', - '!source/main.brs' - ], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes file from non-rootdir top-level pattern', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/../externalDir/source/main.brs`, - [ - '!../externalDir/**/*' - ], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes a file that is negated in src;dest;', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - [ - 'source/**/*', - { - src: '!source/main.brs' - } - ], - rootDir - ) - ).to.be.undefined; - }); - - it('works for brighterscript files', () => { - let destPath = rokuDeploy.getDestPath( - util.standardizePath(`${cwd}/src/source/main.bs`), - [ - 'manifest', - 'source/**/*.bs' - ], - s`${cwd}/src` - ); - expect(s`${destPath}`).to.equal(s`source/main.bs`); - }); - - it('excludes a file found outside the root dir', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/../source/main.brs`, - [ - '../source/**/*' - ], - rootDir - ) - ).to.be.undefined; - }); - }); - - describe('normalizeRootDir', () => { - it('handles falsey values', () => { - expect(rokuDeploy.normalizeRootDir(null)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir(undefined)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir(' ')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('\t')).to.equal(cwd); - }); - - it('handles non-falsey values', () => { - expect(rokuDeploy.normalizeRootDir(cwd)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('./')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('./testProject')).to.equal(path.join(cwd, 'testProject')); - }); - }); - describe('retrieveSignedPackage', () => { let onHandler: any; beforeEach(() => { - sinon.stub(rokuDeploy.fsExtra, 'ensureDir').callsFake(((pth: string, callback: (err: Error) => void) => { + sinon.stub(fsExtra, 'ensureDir').callsFake(((pth: string, callback: (err: Error) => void) => { //do nothing, assume the dir gets created }) as any); @@ -3597,6 +3482,35 @@ describe('index', () => { expect(getToFileIsResolved).to.be.true; }); }); + + function mockDoGetRequest(body = '', statusCode = 200) { + return sinon.stub(rokuDeploy as any, 'doGetRequest').callsFake((params) => { + let results = { response: { statusCode: statusCode }, body: body }; + rokuDeploy['checkRequest'](results); + return Promise.resolve(results); + }); + } + + function mockDoPostRequest(body = '', statusCode = 200) { + return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { + let results = { response: { statusCode: statusCode }, body: body }; + rokuDeploy['checkRequest'](results); + return Promise.resolve(results); + }); + } + + async function assertThrowsAsync(fn) { + let f = () => { }; + try { + await fn(); + } catch (e) { + f = () => { + throw e; + }; + } finally { + assert.throws(f); + } + } }); function getFakeResponseBody(messages: string): string { diff --git a/src/util.spec.ts b/src/util.spec.ts index eeb4726..1705127 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -1,7 +1,7 @@ import { util, standardizePath as s } from './util'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; -import { tempDir, rootDir } from './testUtils.spec'; +import { cwd, tempDir, rootDir } from './testUtils.spec'; import * as path from 'path'; import * as dns from 'dns'; import { createSandbox } from 'sinon'; @@ -336,6 +336,121 @@ describe('util', () => { }); }); + describe('normalizeRootDir', () => { + it('handles falsey values', () => { + expect(util.normalizeRootDir(null)).to.equal(cwd); + expect(util.normalizeRootDir(undefined)).to.equal(cwd); + expect(util.normalizeRootDir('')).to.equal(cwd); + expect(util.normalizeRootDir(' ')).to.equal(cwd); + expect(util.normalizeRootDir('\t')).to.equal(cwd); + }); + + it('handles non-falsey values', () => { + expect(util.normalizeRootDir(cwd)).to.equal(cwd); + expect(util.normalizeRootDir('./')).to.equal(cwd); + expect(util.normalizeRootDir('./testProject')).to.equal(path.join(cwd, 'testProject')); + }); + }); + + describe('getDestPath', () => { + it('handles unrelated exclusions properly', () => { + expect( + util.getDestPath( + s`${rootDir}/components/comp1/comp1.brs`, + [ + '**/*', + '!exclude.me' + ], + rootDir + ) + ).to.equal(s`components/comp1/comp1.brs`); + }); + + it('finds dest path for top-level path', () => { + expect( + util.getDestPath( + s`${rootDir}/components/comp1/comp1.brs`, + ['components/**/*'], + rootDir + ) + ).to.equal(s`components/comp1/comp1.brs`); + }); + + it('does not find dest path for non-matched top-level path', () => { + expect( + util.getDestPath( + s`${rootDir}/source/main.brs`, + ['components/**/*'], + rootDir + ) + ).to.be.undefined; + }); + + it('excludes a file that is negated', () => { + expect( + util.getDestPath( + s`${rootDir}/source/main.brs`, + [ + 'source/**/*', + '!source/main.brs' + ], + rootDir + ) + ).to.be.undefined; + }); + + it('excludes file from non-rootdir top-level pattern', () => { + expect( + util.getDestPath( + s`${rootDir}/../externalDir/source/main.brs`, + [ + '!../externalDir/**/*' + ], + rootDir + ) + ).to.be.undefined; + }); + + it('excludes a file that is negated in src;dest;', () => { + expect( + util.getDestPath( + s`${rootDir}/source/main.brs`, + [ + 'source/**/*', + { + src: '!source/main.brs' + } + ], + rootDir + ) + ).to.be.undefined; + }); + + it('works for brighterscript files', () => { + let destPath = util.getDestPath( + util.standardizePath(`${cwd}/src/source/main.bs`), + [ + 'manifest', + 'source/**/*.bs' + ], + s`${cwd}/src` + ); + expect(s`${destPath}`).to.equal(s`source/main.bs`); + }); + + it('excludes a file found outside the root dir', () => { + expect( + util.getDestPath( + s`${rootDir}/../source/main.brs`, + [ + '../source/**/*' + ], + rootDir + ) + ).to.be.undefined; + }); + }); + describe('getOptionsFromJson', () => { beforeEach(() => { fsExtra.ensureDirSync(rootDir); From 8ae9acec4e28214fb84021e567d801c027b5296f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:45:36 -0500 Subject: [PATCH 30/63] Change signExistingPackage to CreateSignedPackage --- src/RokuDeploy.spec.ts | 14 +++++++------- src/RokuDeploy.ts | 2 +- src/cli.spec.ts | 6 +++--- src/cli.ts | 4 ++-- ...ageCommand.ts => CreateSignedPackageCommand.ts} | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) rename src/commands/{SignExistingPackageCommand.ts => CreateSignedPackageCommand.ts} (72%) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 99056a3..b0ccc07 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1338,14 +1338,14 @@ describe('index', () => { }); }); - describe('signExistingPackage', () => { + describe('createSignedPackage', () => { beforeEach(() => { fsExtra.outputFileSync(`${stagingDir}/manifest`, ``); }); it('should return our error if signingPassword is not supplied', async () => { await expectThrowsAsync(async () => { - await rokuDeploy.signPackage({ + await rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: undefined, @@ -1362,7 +1362,7 @@ describe('index', () => { process.nextTick(callback, error); return {} as any; }); - await rokuDeploy.signPackage({ + await rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1378,7 +1378,7 @@ describe('index', () => { it('should return our error if it received invalid data', async () => { try { mockDoPostRequest(null); - await rokuDeploy.signPackage({ + await rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1399,7 +1399,7 @@ describe('index', () => { mockDoPostRequest(body); await expectThrowsAsync( - rokuDeploy.signPackage({ + rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1415,7 +1415,7 @@ describe('index', () => { node.appendChild(pkgDiv);`; mockDoPostRequest(body); - let pkgPath = await rokuDeploy.signPackage({ + let pkgPath = await rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, @@ -1427,7 +1427,7 @@ describe('index', () => { it('should return our fallback error if neither error or package link was detected', async () => { mockDoPostRequest(); await expectThrowsAsync( - rokuDeploy.signPackage({ + rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index bb3f959..62dc8be 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -1002,7 +1002,7 @@ export interface RekeyDeviceOptions { devId: string; } -export interface SignExistingPackageOptions { +export interface CreateSignedPackageOptions { host: string; password: string; signingPassword: string; diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 71d486b..f151a25 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -8,7 +8,7 @@ import { rokuDeploy } from './index'; import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; -import { SignExistingPackageCommand } from './commands/SignExistingPackageCommand'; +import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; import { DeployCommand } from './commands/DeployCommand'; import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; @@ -141,11 +141,11 @@ describe('cli', () => { }); it('Signs an existing package', async () => { - const stub = sinon.stub(rokuDeploy, 'signExistingPackage').callsFake(async () => { + const stub = sinon.stub(rokuDeploy, 'createSignedPackage').callsFake(async () => { return Promise.resolve(''); }); - const command = new SignExistingPackageCommand(); + const command = new CreateSignedPackageCommand(); await command.run({ host: '1.2.3.4', password: '5536', diff --git a/src/cli.ts b/src/cli.ts index 8be0fba..14aa9d8 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,7 +8,7 @@ import { ZipPackageCommand } from './commands/ZipPackageCommand'; import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; -import { SignExistingPackageCommand } from './commands/SignExistingPackageCommand'; +import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; import { RetrieveSignedPackageCommand } from './commands/RetrieveSignedPackageCommand'; import { DeployCommand } from './commands/DeployCommand'; import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; @@ -140,7 +140,7 @@ void yargs .option('signingPassword', { type: 'string', description: 'The password of the signing key', demandOption: false }) .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }); }, (args: any) => { - return new SignExistingPackageCommand().run(args); + return new CreateSignedPackageCommand().run(args); }) .command('retrieveSignedPackage', 'Retrieve a signed package', (builder) => { diff --git a/src/commands/SignExistingPackageCommand.ts b/src/commands/CreateSignedPackageCommand.ts similarity index 72% rename from src/commands/SignExistingPackageCommand.ts rename to src/commands/CreateSignedPackageCommand.ts index 3b1175e..8488650 100644 --- a/src/commands/SignExistingPackageCommand.ts +++ b/src/commands/CreateSignedPackageCommand.ts @@ -1,8 +1,8 @@ import { rokuDeploy } from '../index'; -export class SignExistingPackageCommand { +export class CreateSignedPackageCommand { async run(args) { - await rokuDeploy.signPackage({ + await rokuDeploy.createSignedPackage({ host: args.host, password: args.password, signingPassword: args.signingPassword, From cc0f4b8e4aed1a3e7cc12789f8ef60108cfb1215 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:47:57 -0500 Subject: [PATCH 31/63] Delete retrieveSignedPackage --- src/RokuDeploy.spec.ts | 105 ----------------------------------------- src/cli.spec.ts | 23 --------- src/cli.ts | 10 ---- 3 files changed, 138 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index b0ccc07..0429510 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -3127,111 +3127,6 @@ describe('index', () => { }); }); - describe('retrieveSignedPackage', () => { - let onHandler: any; - beforeEach(() => { - sinon.stub(fsExtra, 'ensureDir').callsFake(((pth: string, callback: (err: Error) => void) => { - //do nothing, assume the dir gets created - }) as any); - - //intercept the http request - sinon.stub(request, 'get').callsFake(() => { - let req: any = { - on: (event, callback) => { - process.nextTick(() => { - onHandler(event, callback); - }); - return req; - }, - pipe: async () => { - //if a write stream gets created, write some stuff and close it - const writeStream = await writeStreamPromise; - writeStream.write('test'); - writeStream.close(); - } - }; - return req; - }); - }); - - it('returns a pkg file path on success', async () => { - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - let pkgFilePath = await rokuDeploy.retrieveSignedPackage('path_to_pkg', { - host: '1.2.3.4', - outFile: 'roku-deploy-test', - password: 'aaaa' - }); - expect(pkgFilePath).to.equal(path.join(process.cwd(), 'out', 'roku-deploy-test.pkg')); - }); - - it('returns a pkg file path on success', async () => { - //the write stream should return null, which causes a specific branch to be executed - createWriteStreamStub.callsFake(() => { - return null; - }); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - let error: Error; - try { - await rokuDeploy.retrieveSignedPackage('path_to_pkg', { - host: '1.2.3.4', - password: 'password', - outFile: 'roku-deploy-test' - }); - } catch (e) { - error = e as any; - } - expect(error.message.startsWith('Unable to create write stream for')).to.be.true; - }); - - it('throws when error in request is encountered', async () => { - onHandler = (event, callback) => { - if (event === 'error') { - callback(new Error('Some error')); - } - }; - await expectThrowsAsync( - rokuDeploy.retrieveSignedPackage('path_to_pkg', { - host: '1.2.3.4', - outFile: 'roku-deploy-test', - password: 'aaaa' - }), - 'Some error' - ); - }); - - it('throws when status code is non 200', async () => { - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 500 - }); - } - }; - await expectThrowsAsync( - rokuDeploy.retrieveSignedPackage('path_to_pkg', { - host: '1.2.3.4', - outFile: 'roku-deploy-test', - password: 'aaaa' - }), - 'Invalid response code: 500' - ); - }); - }); - describe('checkRequest', () => { it('throws FailedDeviceResponseError when necessary', () => { sinon.stub(rokuDeploy as any, 'getRokuMessagesFromResponseBody').returns({ diff --git a/src/cli.spec.ts b/src/cli.spec.ts index f151a25..eaaf1fc 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -14,7 +14,6 @@ import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannel import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; -import { RetrieveSignedPackageCommand } from './commands/RetrieveSignedPackageCommand'; const sinon = createSandbox(); @@ -163,28 +162,6 @@ describe('cli', () => { }); }); - it('Retrieves a signed package', async () => { - const stub = sinon.stub(rokuDeploy, 'retrieveSignedPackage').callsFake(async () => { - return Promise.resolve(''); - }); - - const command = new RetrieveSignedPackageCommand(); - await command.run({ - pathToPkg: 'path_to_pkg', - host: '1.2.3.4', - password: '5536', - outFile: 'roku-deploy-test' - }); - - expect( - stub.getCall(0).args - ).to.eql(['path_to_pkg', { - host: '1.2.3.4', - password: '5536', - outFile: 'roku-deploy-test' - }]); - }); - it('Deploys a package', async () => { const stub = sinon.stub(rokuDeploy, 'deploy').callsFake(async () => { return Promise.resolve({ diff --git a/src/cli.ts b/src/cli.ts index 14aa9d8..16cfe0a 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,6 @@ import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; -import { RetrieveSignedPackageCommand } from './commands/RetrieveSignedPackageCommand'; import { DeployCommand } from './commands/DeployCommand'; import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; @@ -143,15 +142,6 @@ void yargs return new CreateSignedPackageCommand().run(args); }) - .command('retrieveSignedPackage', 'Retrieve a signed package', (builder) => { - return builder - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) - .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) - .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); - }, (args: any) => { - return new RetrieveSignedPackageCommand().run(args); - }) - .command('deploy', 'Deploy a pre-existing packaged zip file to a remote Roku', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) From cbfcaed14325c5ee0779c1850abdb277ffb18fb8 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:50:13 -0500 Subject: [PATCH 32/63] Move getFilePaths --- src/RokuDeploy.spec.ts | 650 +------------------ src/commands/RetrieveSignedPackageCommand.ts | 11 - src/util.spec.ts | 650 ++++++++++++++++++- src/util.ts | 4 +- 4 files changed, 654 insertions(+), 661 deletions(-) delete mode 100644 src/commands/RetrieveSignedPackageCommand.ts diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 0429510..38ec4f5 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -12,7 +12,7 @@ import type { BeforeZipCallbackInfo } from './RokuDeploy'; import { RokuDeploy } from './RokuDeploy'; import * as errors from './Errors'; import { util, standardizePath as s, standardizePathPosix as sp } from './util'; -import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; +import type { RokuDeployOptions } from './RokuDeployOptions'; import { cwd, expectPathExists, expectPathNotExists, expectThrowsAsync, outDir, rootDir, stagingDir, tempDir, writeFiles } from './testUtils.spec'; import { createSandbox } from 'sinon'; import * as r from 'postman-request'; @@ -641,7 +641,7 @@ describe('index', () => { return Promise.resolve(); }); - sinon.stub(rokuDeploy, 'getFilePaths').returns( + sinon.stub(util, 'getFilePaths').returns( Promise.resolve([ { src: s`${rootDir}/source/main.brs`, @@ -1742,7 +1742,7 @@ describe('index', () => { let stagingDirValue = rokuDeploy.getOptions(opts).stagingDir; //getFilePaths detects the file - expect(await rokuDeploy.getFilePaths(['renamed_test.md'], opts.rootDir)).to.eql([{ + expect(await util.getFilePaths(['renamed_test.md'], opts.rootDir)).to.eql([{ src: s`${opts.rootDir}/renamed_test.md`, dest: s`renamed_test.md` }]); @@ -1787,7 +1787,7 @@ describe('index', () => { let stagingPath = rokuDeploy.getOptions(opts).stagingDir; //getFilePaths detects the file expect( - (await rokuDeploy.getFilePaths(opts.files, opts.rootDir)).sort((a, b) => a.src.localeCompare(b.src)) + (await util.getFilePaths(opts.files, opts.rootDir)).sort((a, b) => a.src.localeCompare(b.src)) ).to.eql([{ src: s`${tempDir}/mainProject/source/lib/lib.brs`, dest: s`source/lib/lib.brs` @@ -2477,648 +2477,6 @@ describe('index', () => { }); }); - describe('getFilePaths', () => { - const otherProjectName = 'otherProject'; - const otherProjectDir = sp`${rootDir}/../${otherProjectName}`; - //create baseline project structure - beforeEach(() => { - fsExtra.ensureDirSync(`${rootDir}/components/emptyFolder`); - writeFiles(rootDir, [ - `manifest`, - `source/main.brs`, - `source/lib.brs`, - `components/component1.xml`, - `components/component1.brs`, - `components/screen1/screen1.xml`, - `components/screen1/screen1.brs` - ]); - }); - - async function getFilePaths(files: FileEntry[], rootDirOverride = rootDir) { - return (await rokuDeploy.getFilePaths(files, rootDirOverride)) - .sort((a, b) => a.src.localeCompare(b.src)); - } - - describe('top-level-patterns', () => { - it('excludes a file that is negated', async () => { - expect(await getFilePaths([ - 'source/**/*', - '!source/main.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('excludes file from non-rootdir top-level pattern', async () => { - writeFiles(rootDir, ['../externalDir/source/main.brs']); - expect(await getFilePaths([ - '../externalDir/**/*', - '!../externalDir/**/*' - ])).to.eql([]); - }); - - it('throws when using top-level string referencing file outside the root dir', async () => { - writeFiles(rootDir, [`../source/main.brs`]); - await expectThrowsAsync(async () => { - await getFilePaths([ - '../source/**/*' - ]); - }, 'Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); - }); - - it('works for brighterscript files', async () => { - writeFiles(rootDir, ['src/source/main.bs']); - expect(await getFilePaths([ - 'manifest', - 'source/**/*.bs' - ], s`${rootDir}/src`)).to.eql([{ - src: s`${rootDir}/src/source/main.bs`, - dest: s`source/main.bs` - }]); - }); - - it('works for root-level double star in top-level pattern', async () => { - expect(await getFilePaths([ - '**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for multile entries', async () => { - expect(await getFilePaths([ - 'source/**/*', - 'components/**/*', - 'manifest' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies top-level-string single-star globs', async () => { - writeFiles(rootDir, [ - 'source/lib.brs', - 'source/main.brs' - ]); - expect(await getFilePaths([ - 'source/*.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for double-star globs', async () => { - expect(await getFilePaths([ - '**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies subdir-level relative double-star globs', async () => { - expect(await getFilePaths([ - 'components/**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }]); - }); - - it('Finds folder using square brackets glob pattern', async () => { - fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); - expect(await getFilePaths([ - '[test]/*' - ], - rootDir - )).to.eql([{ - src: s`${rootDir}/e/file.brs`, - dest: s`e/file.brs` - }]); - }); - - it('Finds folder with escaped square brackets glob pattern as name', async () => { - fsExtra.outputFileSync(`${rootDir}/[test]/file.brs`, ''); - fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); - expect(await getFilePaths([ - '\\[test\\]/*' - ], - rootDir - )).to.eql([{ - src: s`${rootDir}/[test]/file.brs`, - dest: s`[test]/file.brs` - }]); - }); - - it('throws exception when top-level strings reference files not under rootDir', async () => { - writeFiles(otherProjectDir, [ - 'manifest' - ]); - await expectThrowsAsync( - getFilePaths([ - `../${otherProjectName}/**/*` - ]) - ); - }); - - it('applies negated patterns', async () => { - expect(await getFilePaths([ - //include all components - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - - it('handles negated multi-globs', async () => { - expect((await getFilePaths([ - 'components/**/*', - '!components/screen1/**/*' - ])).map(x => x.dest)).to.eql([ - s`components/component1.brs`, - s`components/component1.xml` - ]); - }); - - it('allows negating paths outside rootDir without requiring src;dest; syntax', async () => { - fsExtra.outputFileSync(`${rootDir}/../externalLib/source/lib.brs`, ''); - const filePaths = await getFilePaths([ - 'source/**/*', - { src: '../externalLib/**/*', dest: 'source' }, - '!../externalLib/source/**/*' - ], rootDir); - expect( - filePaths.map(x => s`${x.src}`).sort() - ).to.eql([ - s`${rootDir}/source/lib.brs`, - s`${rootDir}/source/main.brs` - ]); - }); - - it('applies multi-glob paths relative to rootDir', async () => { - expect(await getFilePaths([ - 'manifest', - 'source/**/*', - 'components/**/*', - '!components/scenes/**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('ignores non-glob folder paths', async () => { - expect(await getFilePaths([ - //this is the folder called "components" - 'components' - ])).to.eql([]); //there should be no matches because rokudeploy ignores folders - }); - - }); - - describe('{src;dest} objects', () => { - it('excludes a file that is negated in src;dest;', async () => { - expect(await getFilePaths([ - 'source/**/*', - { - src: '!source/main.brs' - } - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('works for root-level double star in {src;dest} object', async () => { - expect(await getFilePaths([{ - src: '**/*', - dest: '' - } - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('uses the root of staging folder for dest when not specified with star star', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/**/*` - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`source/thirdPartyLib.brs` - }]); - }); - - it('copies absolute path files to specified dest', async () => { - writeFiles(otherProjectDir, [ - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/source/thirdPartyLib.brs`, - dest: 'lib/thirdPartyLib.brs' - }])).to.eql([{ - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`lib/thirdPartyLib.brs` - }]); - }); - - it('copies relative path files to specified dest', async () => { - const rootDirName = path.basename(rootDir); - writeFiles(rootDir, [ - 'source/main.brs' - ]); - expect(await getFilePaths([{ - src: `../${rootDirName}/source/main.brs`, - dest: 'source/main.brs' - }])).to.eql([{ - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('maintains relative path after **', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `../otherProject/**/*`, - dest: 'outFolder/' - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`outFolder/components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`outFolder/manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`outFolder/source/thirdPartyLib.brs` - }]); - }); - - it('works for other globs', async () => { - expect(await getFilePaths([{ - src: `components/screen1/*creen1.brs`, - dest: s`/source` - }])).to.eql([{ - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`source/screen1.brs` - }]); - }); - - it('applies negated patterns', async () => { - writeFiles(rootDir, [ - 'components/component1.brs', - 'components/component1.xml', - 'components/screen1/screen1.brs', - 'components/screen1/screen1.xml' - ]); - expect(await getFilePaths([ - //include all component brs files - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - }); - - it('converts relative rootDir path to absolute', async () => { - let stub = sinon.stub(rokuDeploy, 'getOptions').callThrough(); - await getFilePaths([ - 'source/main.brs' - ], './rootDir'); - expect(stub.callCount).to.be.greaterThan(0); - expect(stub.getCall(0).args[0].rootDir).to.eql('./rootDir'); - expect(stub.getCall(0).returnValue.rootDir).to.eql(s`${cwd}/rootDir`); - }); - - it('works when using a different current working directory than rootDir', async () => { - writeFiles(rootDir, [ - 'manifest', - 'images/splash_hd.jpg' - ]); - //sanity check, make sure it works without fiddling with cwd intact - let paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); - - //change the working directory and verify everything still works - - let wrongCwd = path.dirname(path.resolve(options.rootDir)); - process.chdir(wrongCwd); - - paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); - }); - - it('supports absolute paths from outside of the rootDir', async () => { - options = rokuDeploy.getOptions(options); - - //dest not specified - expect(await rokuDeploy.getFilePaths([{ - src: sp`${cwd}/README.md` - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`README.md` - }]); - - //dest specified - expect(await rokuDeploy.getFilePaths([{ - src: sp`${cwd}/README.md`, - dest: 'docs/README.md' - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - let paths: any[]; - - paths = await rokuDeploy.getFilePaths([{ - src: sp`${cwd}/README.md`, - dest: s`docs/README.md` - }], outDir); - - expect(paths).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - //top-level string paths pointing to files outside the root should thrown an exception - await expectThrowsAsync(async () => { - paths = await rokuDeploy.getFilePaths([ - sp`${cwd}/README.md` - ], outDir); - }); - }); - - it('supports relative paths that grab files from outside of the rootDir', async () => { - writeFiles(`${rootDir}/../`, [ - 'README.md' - ]); - expect( - await rokuDeploy.getFilePaths([{ - src: sp`../README.md` - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`README.md` - }]); - - expect( - await rokuDeploy.getFilePaths([{ - src: sp`../README.md`, - dest: 'docs/README.md' - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`docs/README.md` - }]); - }); - - it('should throw exception because we cannot have top-level string paths pointed to files outside the root', async () => { - writeFiles(rootDir, [ - '../README.md' - ]); - await expectThrowsAsync( - rokuDeploy.getFilePaths([ - path.posix.join('..', 'README.md') - ], outDir) - ); - }); - - it('supports overriding paths', async () => { - let paths = await rokuDeploy.getFilePaths([{ - src: sp`${rootDir}/components/component1.brs`, - dest: 'comp1.brs' - }, { - src: sp`${rootDir}/components/screen1/screen1.brs`, - dest: 'comp1.brs' - }], rootDir); - expect(paths).to.be.lengthOf(1); - expect(s`${paths[0].src}`).to.equal(s`${rootDir}/components/screen1/screen1.brs`); - }); - - it('supports overriding paths from outside the root dir', async () => { - let thisRootDir = s`${tempDir}/tempTestOverrides/src`; - try { - - fsExtra.ensureDirSync(s`${thisRootDir}/source`); - fsExtra.ensureDirSync(s`${thisRootDir}/components`); - fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); - - fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); - - let files = [ - '**/*.xml', - '**/*.brs', - { - src: '../.tmp/MainScene.brs', - dest: 'components/MainScene.brs' - } - ]; - let paths = await rokuDeploy.getFilePaths(files, thisRootDir); - - //the MainScene.brs file from source should NOT be included - let mainSceneEntries = paths.filter(x => s`${x.dest}` === s`components/MainScene.brs`); - expect( - mainSceneEntries, - `Should only be one files entry for 'components/MainScene.brs'` - ).to.be.lengthOf(1); - expect(s`${mainSceneEntries[0].src}`).to.eql(s`${thisRootDir}/../.tmp/MainScene.brs`); - } finally { - //clean up - await fsExtra.remove(s`${thisRootDir}/../`); - } - }); - - it('maintains original file path', async () => { - fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); - expect( - await rokuDeploy.getFilePaths([ - 'components/CustomButton.brs' - ], rootDir) - ).to.eql([{ - src: s`${rootDir}/components/CustomButton.brs`, - dest: s`components/CustomButton.brs` - }]); - }); - - it('correctly assumes file path if not given', async () => { - fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); - expect( - (await rokuDeploy.getFilePaths([ - { src: 'components/*' } - ], rootDir)).sort((a, b) => a.src.localeCompare(b.src)) - ).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/CustomButton.brs`, - dest: s`components/CustomButton.brs` - }]); - }); - }); - describe('computeFileDestPath', () => { it('treats {src;dest} without dest as a top-level string', () => { expect( diff --git a/src/commands/RetrieveSignedPackageCommand.ts b/src/commands/RetrieveSignedPackageCommand.ts deleted file mode 100644 index 411dfd9..0000000 --- a/src/commands/RetrieveSignedPackageCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { rokuDeploy } from '../index'; - -export class RetrieveSignedPackageCommand { - async run(args) { - await rokuDeploy.retrieveSignedPackage(args.pathToPkg, { - host: args.host, - password: args.password, - outFile: args.outFile - }); - } -} diff --git a/src/util.spec.ts b/src/util.spec.ts index 1705127..ea34990 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -1,10 +1,12 @@ -import { util, standardizePath as s } from './util'; +import { util, standardizePath as s, standardizePathPosix as sp } from './util'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; -import { cwd, tempDir, rootDir } from './testUtils.spec'; +import { cwd, tempDir, rootDir, outDir, expectThrowsAsync, writeFiles } from './testUtils.spec'; import * as path from 'path'; import * as dns from 'dns'; import { createSandbox } from 'sinon'; +import { RokuDeploy } from './RokuDeploy'; +import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; const sinon = createSandbox(); describe('util', () => { @@ -451,6 +453,650 @@ describe('util', () => { }); }); + describe('getFilePaths', () => { + const otherProjectName = 'otherProject'; + const otherProjectDir = sp`${rootDir}/../${otherProjectName}`; + let rokuDeploy: RokuDeploy; + let options: RokuDeployOptions; + //create baseline project structure + beforeEach(() => { + rokuDeploy = new RokuDeploy(); + options = rokuDeploy.getOptions({}); + fsExtra.ensureDirSync(`${rootDir}/components/emptyFolder`); + writeFiles(rootDir, [ + `manifest`, + `source/main.brs`, + `source/lib.brs`, + `components/component1.xml`, + `components/component1.brs`, + `components/screen1/screen1.xml`, + `components/screen1/screen1.brs` + ]); + }); + + async function getFilePaths(files: FileEntry[], rootDirOverride = rootDir) { + return (await util.getFilePaths(files, rootDirOverride)) + .sort((a, b) => a.src.localeCompare(b.src)); + } + + describe('top-level-patterns', () => { + it('excludes a file that is negated', async () => { + expect(await getFilePaths([ + 'source/**/*', + '!source/main.brs' + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }]); + }); + + it('excludes file from non-rootdir top-level pattern', async () => { + writeFiles(rootDir, ['../externalDir/source/main.brs']); + expect(await getFilePaths([ + '../externalDir/**/*', + '!../externalDir/**/*' + ])).to.eql([]); + }); + + it('throws when using top-level string referencing file outside the root dir', async () => { + writeFiles(rootDir, [`../source/main.brs`]); + await expectThrowsAsync(async () => { + await getFilePaths([ + '../source/**/*' + ]); + }, 'Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); + }); + + it('works for brighterscript files', async () => { + writeFiles(rootDir, ['src/source/main.bs']); + expect(await getFilePaths([ + 'manifest', + 'source/**/*.bs' + ], s`${rootDir}/src`)).to.eql([{ + src: s`${rootDir}/src/source/main.bs`, + dest: s`source/main.bs` + }]); + }); + + it('works for root-level double star in top-level pattern', async () => { + expect(await getFilePaths([ + '**/*' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, + { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, + { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, + { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, + { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, + { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('works for multile entries', async () => { + expect(await getFilePaths([ + 'source/**/*', + 'components/**/*', + 'manifest' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('copies top-level-string single-star globs', async () => { + writeFiles(rootDir, [ + 'source/lib.brs', + 'source/main.brs' + ]); + expect(await getFilePaths([ + 'source/*.brs' + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('works for double-star globs', async () => { + expect(await getFilePaths([ + '**/*.brs' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('copies subdir-level relative double-star globs', async () => { + expect(await getFilePaths([ + 'components/**/*.brs' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }]); + }); + + it('Finds folder using square brackets glob pattern', async () => { + fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); + expect(await getFilePaths([ + '[test]/*' + ], + rootDir + )).to.eql([{ + src: s`${rootDir}/e/file.brs`, + dest: s`e/file.brs` + }]); + }); + + it('Finds folder with escaped square brackets glob pattern as name', async () => { + fsExtra.outputFileSync(`${rootDir}/[test]/file.brs`, ''); + fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); + expect(await getFilePaths([ + '\\[test\\]/*' + ], + rootDir + )).to.eql([{ + src: s`${rootDir}/[test]/file.brs`, + dest: s`[test]/file.brs` + }]); + }); + + it('throws exception when top-level strings reference files not under rootDir', async () => { + writeFiles(otherProjectDir, [ + 'manifest' + ]); + await expectThrowsAsync( + getFilePaths([ + `../${otherProjectName}/**/*` + ]) + ); + }); + + it('applies negated patterns', async () => { + expect(await getFilePaths([ + //include all components + 'components/**/*.brs', + //exclude all xml files + '!components/**/*.xml', + //re-include a specific xml file + 'components/screen1/screen1.xml' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }]); + }); + + it('handles negated multi-globs', async () => { + expect((await getFilePaths([ + 'components/**/*', + '!components/screen1/**/*' + ])).map(x => x.dest)).to.eql([ + s`components/component1.brs`, + s`components/component1.xml` + ]); + }); + + it('allows negating paths outside rootDir without requiring src;dest; syntax', async () => { + fsExtra.outputFileSync(`${rootDir}/../externalLib/source/lib.brs`, ''); + const filePaths = await getFilePaths([ + 'source/**/*', + { src: '../externalLib/**/*', dest: 'source' }, + '!../externalLib/source/**/*' + ], rootDir); + expect( + filePaths.map(x => s`${x.src}`).sort() + ).to.eql([ + s`${rootDir}/source/lib.brs`, + s`${rootDir}/source/main.brs` + ]); + }); + + it('applies multi-glob paths relative to rootDir', async () => { + expect(await getFilePaths([ + 'manifest', + 'source/**/*', + 'components/**/*', + '!components/scenes/**/*' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('ignores non-glob folder paths', async () => { + expect(await getFilePaths([ + //this is the folder called "components" + 'components' + ])).to.eql([]); //there should be no matches because rokudeploy ignores folders + }); + + }); + + describe('{src;dest} objects', () => { + it('excludes a file that is negated in src;dest;', async () => { + expect(await getFilePaths([ + 'source/**/*', + { + src: '!source/main.brs' + } + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }]); + }); + + it('works for root-level double star in {src;dest} object', async () => { + expect(await getFilePaths([{ + src: '**/*', + dest: '' + } + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, + { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, + { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, + { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, + { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, + { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('uses the root of staging folder for dest when not specified with star star', async () => { + writeFiles(otherProjectDir, [ + 'components/component1/subComponent/screen.brs', + 'manifest', + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `${otherProjectDir}/**/*` + }])).to.eql([{ + src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, + dest: s`components/component1/subComponent/screen.brs` + }, { + src: s`${otherProjectDir}/manifest`, + dest: s`manifest` + }, { + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`source/thirdPartyLib.brs` + }]); + }); + + it('copies absolute path files to specified dest', async () => { + writeFiles(otherProjectDir, [ + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `${otherProjectDir}/source/thirdPartyLib.brs`, + dest: 'lib/thirdPartyLib.brs' + }])).to.eql([{ + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`lib/thirdPartyLib.brs` + }]); + }); + + it('copies relative path files to specified dest', async () => { + const rootDirName = path.basename(rootDir); + writeFiles(rootDir, [ + 'source/main.brs' + ]); + expect(await getFilePaths([{ + src: `../${rootDirName}/source/main.brs`, + dest: 'source/main.brs' + }])).to.eql([{ + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('maintains relative path after **', async () => { + writeFiles(otherProjectDir, [ + 'components/component1/subComponent/screen.brs', + 'manifest', + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `../otherProject/**/*`, + dest: 'outFolder/' + }])).to.eql([{ + src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, + dest: s`outFolder/components/component1/subComponent/screen.brs` + }, { + src: s`${otherProjectDir}/manifest`, + dest: s`outFolder/manifest` + }, { + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`outFolder/source/thirdPartyLib.brs` + }]); + }); + + it('works for other globs', async () => { + expect(await getFilePaths([{ + src: `components/screen1/*creen1.brs`, + dest: s`/source` + }])).to.eql([{ + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`source/screen1.brs` + }]); + }); + + it('applies negated patterns', async () => { + writeFiles(rootDir, [ + 'components/component1.brs', + 'components/component1.xml', + 'components/screen1/screen1.brs', + 'components/screen1/screen1.xml' + ]); + expect(await getFilePaths([ + //include all component brs files + 'components/**/*.brs', + //exclude all xml files + '!components/**/*.xml', + //re-include a specific xml file + 'components/screen1/screen1.xml' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }]); + }); + }); + + it('converts relative rootDir path to absolute', async () => { + let stub = sinon.stub(rokuDeploy, 'getOptions').callThrough(); + await getFilePaths([ + 'source/main.brs' + ], './rootDir'); + expect(stub.callCount).to.be.greaterThan(0); + expect(stub.getCall(0).args[0].rootDir).to.eql('./rootDir'); + expect(stub.getCall(0).returnValue.rootDir).to.eql(s`${cwd}/rootDir`); + }); + + it('works when using a different current working directory than rootDir', async () => { + writeFiles(rootDir, [ + 'manifest', + 'images/splash_hd.jpg' + ]); + //sanity check, make sure it works without fiddling with cwd intact + let paths = await getFilePaths([ + 'manifest', + 'images/splash_hd.jpg' + ]); + + expect(paths).to.eql([{ + src: s`${rootDir}/images/splash_hd.jpg`, + dest: s`images/splash_hd.jpg` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }]); + + //change the working directory and verify everything still works + + let wrongCwd = path.dirname(path.resolve(options.rootDir)); + process.chdir(wrongCwd); + + paths = await getFilePaths([ + 'manifest', + 'images/splash_hd.jpg' + ]); + + expect(paths).to.eql([{ + src: s`${rootDir}/images/splash_hd.jpg`, + dest: s`images/splash_hd.jpg` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }]); + }); + + it('supports absolute paths from outside of the rootDir', async () => { + //dest not specified + expect(await getFilePaths([{ + src: sp`${cwd}/README.md` + }], options.rootDir)).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`README.md` + }]); + + //dest specified + expect(await getFilePaths([{ + src: sp`${cwd}/README.md`, + dest: 'docs/README.md' + }], options.rootDir)).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`docs/README.md` + }]); + + let paths: any[]; + + paths = await getFilePaths([{ + src: sp`${cwd}/README.md`, + dest: s`docs/README.md` + }], outDir); + + expect(paths).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`docs/README.md` + }]); + + //top-level string paths pointing to files outside the root should thrown an exception + await expectThrowsAsync(async () => { + paths = await getFilePaths([ + sp`${cwd}/README.md` + ], outDir); + }); + }); + + it('supports relative paths that grab files from outside of the rootDir', async () => { + writeFiles(`${rootDir}/../`, [ + 'README.md' + ]); + expect( + await getFilePaths([{ + src: sp`../README.md` + }], rootDir) + ).to.eql([{ + src: s`${rootDir}/../README.md`, + dest: s`README.md` + }]); + + expect( + await getFilePaths([{ + src: sp`../README.md`, + dest: 'docs/README.md' + }], rootDir) + ).to.eql([{ + src: s`${rootDir}/../README.md`, + dest: s`docs/README.md` + }]); + }); + + it('should throw exception because we cannot have top-level string paths pointed to files outside the root', async () => { + writeFiles(rootDir, [ + '../README.md' + ]); + await expectThrowsAsync( + getFilePaths([ + path.posix.join('..', 'README.md') + ], outDir) + ); + }); + + it('supports overriding paths', async () => { + let paths = await getFilePaths([{ + src: sp`${rootDir}/components/component1.brs`, + dest: 'comp1.brs' + }, { + src: sp`${rootDir}/components/screen1/screen1.brs`, + dest: 'comp1.brs' + }], rootDir); + expect(paths).to.be.lengthOf(1); + expect(s`${paths[0].src}`).to.equal(s`${rootDir}/components/screen1/screen1.brs`); + }); + + it('supports overriding paths from outside the root dir', async () => { + let thisRootDir = s`${tempDir}/tempTestOverrides/src`; + try { + + fsExtra.ensureDirSync(s`${thisRootDir}/source`); + fsExtra.ensureDirSync(s`${thisRootDir}/components`); + fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); + + fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); + + let files = [ + '**/*.xml', + '**/*.brs', + { + src: '../.tmp/MainScene.brs', + dest: 'components/MainScene.brs' + } + ]; + let paths = await getFilePaths(files, thisRootDir); + + //the MainScene.brs file from source should NOT be included + let mainSceneEntries = paths.filter(x => s`${x.dest}` === s`components/MainScene.brs`); + expect( + mainSceneEntries, + `Should only be one files entry for 'components/MainScene.brs'` + ).to.be.lengthOf(1); + expect(s`${mainSceneEntries[0].src}`).to.eql(s`${thisRootDir}/../.tmp/MainScene.brs`); + } finally { + //clean up + await fsExtra.remove(s`${thisRootDir}/../`); + } + }); + + it('maintains original file path', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + await getFilePaths([ + 'components/CustomButton.brs' + ], rootDir) + ).to.eql([{ + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }]); + }); + + it('correctly assumes file path if not given', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + (await getFilePaths([ + { src: 'components/*' } + ], rootDir)).sort((a, b) => a.src.localeCompare(b.src)) + ).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }]); + }); + }); + describe('getOptionsFromJson', () => { beforeEach(() => { fsExtra.ensureDirSync(rootDir); diff --git a/src/util.ts b/src/util.ts index 9fd883b..7c1846d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -291,7 +291,7 @@ export class Util { public async getFilePaths(files: FileEntry[], rootDir: string): Promise { //if the rootDir isn't absolute, convert it to absolute using the standard options flow if (path.isAbsolute(rootDir) === false) { - rootDir = this.getOptions({ rootDir: rootDir }).rootDir; + rootDir = rokuDeploy.getOptions({ rootDir: rootDir }).rootDir; //TODO: This moved from rokudeploy to here, but if we need to get options how do we fix this? } const entries = this.normalizeFilesArray(files); const srcPathsByIndex = await util.globAllByIndex( @@ -486,7 +486,7 @@ export class Util { /** * A function to fill in any missing arguments with JSON values */ - public getOptionsFromJson(defaultArgs) { //TODO: create a test for this in cli.spec.ts too + public getOptionsFromJson(defaultArgs) {//TODO: The original function handled parse errors, but this one doesn't let args = { ...defaultArgs }; const fileNames = ['rokudeploy.json', 'bsconfig.json']; From c74ef8e9f01974ebd1154f95d573d0c811ece6eb Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:51:48 -0500 Subject: [PATCH 33/63] Random changes I forgot to put in the other pushes --- src/RokuDeploy.spec.ts | 2 +- src/RokuDeploy.ts | 23 ++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 38ec4f5..c4f44b8 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -56,7 +56,7 @@ describe('index', () => { writeStreamPromise = writeStreamDeferred.promise as any; //fake out the write stream function - createWriteStreamStub = sinon.stub(rokuDeploy.fsExtra, 'createWriteStream').callsFake((filePath: PathLike) => { + createWriteStreamStub = sinon.stub(fsExtra, 'createWriteStream').callsFake((filePath: PathLike) => { const writeStream = fs.createWriteStream(filePath); writeStreamDeferred.resolve(writeStream); writeStreamDeferred.isComplete = true; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 62dc8be..ad94f4d 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -48,7 +48,7 @@ export class RokuDeploy { throw new Error(`rootDir does not exist at "${rootDir}"`); } - let fileObjects = await this.getFilePaths(options.files, rootDir); + let fileObjects = await util.getFilePaths(options.files, rootDir); //copy all of the files await Promise.all(fileObjects.map(async (fileObject) => { let destFilePath = util.standardizePath(`${stagingPath}/${fileObject.dest}`); @@ -370,7 +370,7 @@ export class RokuDeploy { * Sign a pre-existing package using Roku and return path to retrieve it * @param options */ - public async createSignedPackage(options: SignExistingPackageOptions): Promise { + public async createSignedPackage(options: CreateSignedPackageOptions): Promise { options = this.getOptions(options) as any; if (!options.signingPassword) { throw new errors.MissingRequiredOptionError('Must supply signingPassword'); @@ -1013,15 +1013,6 @@ export interface CreateSignedPackageOptions { devId?: string; } -export interface RetrieveSignedPackageOptions { - host: string; - password: string; - packagePort?: number; - timeout?: number; - username?: string; - outDir?: string; - outFile?: string; -} export interface DeleteInstalledChannelOptions { host: string; password: string; @@ -1043,16 +1034,6 @@ export interface DeployOptions { outDir?: string; } -export interface DeployAndSignPackageOptions { - host: string; - password: string; - signingPassword: string; - rootDir?: string; - files?: FileEntry[]; - retainStagingDir?: boolean; - convertToSquashfs?: boolean; - stagingDir?: string; -} export interface GetOutputPkgFilePathOptions { outFile?: string; outDir?: string; From 583d9608ced0926e8686056e2422c44608c7d927 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:55:21 -0500 Subject: [PATCH 34/63] Change to different notation since getOutputZipFilePath and getOutputPkgPath were made private --- src/RokuDeploy.spec.ts | 10 +++++----- src/commands/GetOutputPkgFilePathCommand.ts | 3 ++- src/commands/GetOutputZipFilePathCommand.ts | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index c4f44b8..9facc3b 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -82,7 +82,7 @@ describe('index', () => { describe('getOutputPkgFilePath', () => { it('should return correct path if given basename', () => { - let outputPath = rokuDeploy.getOutputPkgFilePath({ + let outputPath = rokuDeploy['getOutputPkgFilePath']({ outFile: 'roku-deploy', outDir: outDir }); @@ -90,7 +90,7 @@ describe('index', () => { }); it('should return correct path if given outFile option ending in .zip', () => { - let outputPath = rokuDeploy.getOutputPkgFilePath({ + let outputPath = rokuDeploy['getOutputPkgFilePath']({ outFile: 'roku-deploy.zip', outDir: outDir }); @@ -100,7 +100,7 @@ describe('index', () => { describe('getOutputZipFilePath', () => { it('should return correct path if given basename', () => { - let outputPath = rokuDeploy.getOutputZipFilePath({ + let outputPath = rokuDeploy['getOutputZipFilePath']({ outFile: 'roku-deploy', outDir: outDir }); @@ -108,7 +108,7 @@ describe('index', () => { }); it('should return correct path if given outFile option ending in .zip', () => { - let outputPath = rokuDeploy.getOutputZipFilePath({ + let outputPath = rokuDeploy['getOutputZipFilePath']({ outFile: 'roku-deploy.zip', outDir: outDir }); @@ -866,7 +866,7 @@ describe('index', () => { outDir: outDir, outFile: 'fileThatDoesNotExist.zip' }); - }, `Cannot publish because file does not exist at '${rokuDeploy.getOutputZipFilePath({ + }, `Cannot publish because file does not exist at '${rokuDeploy['getOutputZipFilePath']({ outFile: 'fileThatDoesNotExist.zip', outDir: outDir })}'`); diff --git a/src/commands/GetOutputPkgFilePathCommand.ts b/src/commands/GetOutputPkgFilePathCommand.ts index 78fff13..0861d1e 100644 --- a/src/commands/GetOutputPkgFilePathCommand.ts +++ b/src/commands/GetOutputPkgFilePathCommand.ts @@ -2,7 +2,8 @@ import { rokuDeploy } from '../index'; export class GetOutputPkgFilePathCommand { run(args) { - const outputPath = rokuDeploy.getOutputPkgFilePath({ + // eslint-disable-next-line @typescript-eslint/dot-notation + const outputPath = rokuDeploy['getOutputPkgPath']({ outFile: args.outFile, outDir: args.outDir }); diff --git a/src/commands/GetOutputZipFilePathCommand.ts b/src/commands/GetOutputZipFilePathCommand.ts index 5525057..bd9d7de 100644 --- a/src/commands/GetOutputZipFilePathCommand.ts +++ b/src/commands/GetOutputZipFilePathCommand.ts @@ -2,7 +2,8 @@ import { rokuDeploy } from '../index'; export class GetOutputZipFilePathCommand { run(args) { - const outputPath = rokuDeploy.getOutputZipFilePath({ + // eslint-disable-next-line @typescript-eslint/dot-notation + const outputPath = rokuDeploy['getOutputZipFilePath']({ outFile: args.outFile, outDir: args.outDir }); From 6d874a669943e8b24eb4c9e854cd89cd632ddc9c Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 09:57:45 -0500 Subject: [PATCH 35/63] Change notation for normalizeDeviceInfoFieldValue since it was made privete --- src/RokuDeploy.spec.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 9facc3b..1d39c3f 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -574,24 +574,24 @@ describe('index', () => { describe('normalizeDeviceInfoFieldValue', () => { it('converts normal values', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('true')).to.eql(true); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('false')).to.eql(false); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1')).to.eql(1); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1.2')).to.eql(1.2); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('true')).to.eql(true); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('false')).to.eql(false); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('1')).to.eql(1); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('1.2')).to.eql(1.2); //it'll trim whitespace too - expect(rokuDeploy.normalizeDeviceInfoFieldValue(' 1.2')).to.eql(1.2); - expect(rokuDeploy.normalizeDeviceInfoFieldValue(' 1.2 ')).to.eql(1.2); + expect(rokuDeploy['normalizeDeviceInfoFieldValue'](' 1.2')).to.eql(1.2); + expect(rokuDeploy['normalizeDeviceInfoFieldValue'](' 1.2 ')).to.eql(1.2); }); it('leaves invalid numbers as strings', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('v1.2.3')).to.eql('v1.2.3'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1.2.3-alpha.1')).to.eql('1.2.3-alpha.1'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('123Four')).to.eql('123Four'); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('v1.2.3')).to.eql('v1.2.3'); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('1.2.3-alpha.1')).to.eql('1.2.3-alpha.1'); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('123Four')).to.eql('123Four'); }); it('decodes HTML entities', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('3&4')).to.eql('3&4'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('3&4')).to.eql('3&4'); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('3&4')).to.eql('3&4'); + expect(rokuDeploy['normalizeDeviceInfoFieldValue']('3&4')).to.eql('3&4'); }); }); From f5ba8397678a8ea5e0a7b860ddf6c63462b8d850 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 10:00:54 -0500 Subject: [PATCH 36/63] Change notation for parsedManifestFromString and ParseManifest since being made private --- src/RokuDeploy.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 1d39c3f..be55726 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -2419,13 +2419,13 @@ describe('index', () => { describe('parseManifest', () => { it('correctly parses valid manifest', async () => { fsExtra.outputFileSync(`${rootDir}/manifest`, `title=AwesomeApp`); - let parsedManifest = await rokuDeploy.parseManifest(`${rootDir}/manifest`); + let parsedManifest = await rokuDeploy['parseManifest'](`${rootDir}/manifest`); expect(parsedManifest.title).to.equal('AwesomeApp'); }); it('Throws our error message for a missing file', async () => { await expectThrowsAsync( - rokuDeploy.parseManifest('invalid-path'), + rokuDeploy['parseManifest']('invalid-path'), `invalid-path does not exist` ); }); @@ -2433,7 +2433,7 @@ describe('index', () => { describe('parseManifestFromString', () => { it('correctly parses valid manifest', () => { - let parsedManifest = rokuDeploy.parseManifestFromString(` + let parsedManifest = rokuDeploy['parseManifestFromString'](` title=RokuDeployTestChannel major_version=1 minor_version=0 @@ -2458,7 +2458,7 @@ describe('index', () => { it('correctly converts back to a valid manifest when lineNumber and keyIndexes are provided', () => { expect( rokuDeploy.stringifyManifest( - rokuDeploy.parseManifestFromString('major_version=3\nminor_version=4') + rokuDeploy['parseManifestFromString']('major_version=3\nminor_version=4') ) ).to.equal( 'major_version=3\nminor_version=4' @@ -2466,10 +2466,10 @@ describe('index', () => { }); it('correctly converts back to a valid manifest when lineNumber and keyIndexes are not provided', () => { - const parsed = rokuDeploy.parseManifestFromString('title=App\nmajor_version=3'); + const parsed = rokuDeploy['parseManifestFromString']('title=App\nmajor_version=3'); delete parsed.keyIndexes; delete parsed.lineCount; - let outputParsedManifest = rokuDeploy.parseManifestFromString( + let outputParsedManifest = rokuDeploy['parseManifestFromString']( rokuDeploy.stringifyManifest(parsed) ); expect(outputParsedManifest.title).to.equal('App'); From 2858dd619811b3f05eee98912f95968e7e9d46e2 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Thu, 29 Feb 2024 10:19:09 -0500 Subject: [PATCH 37/63] Change deleteInstalledChannel to deleteDevChannel, other small changes --- src/RokuDeploy.ts | 8 ++++---- src/cli.spec.ts | 2 +- src/device.spec.ts | 12 +----------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index ad94f4d..6970d0d 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -556,7 +556,7 @@ export class RokuDeploy { * Deletes any installed dev channel on the target Roku device * @param options */ - public async deleteDevChannel(options?: DeleteInstalledChannelOptions) { + public async deleteDevChannel(options?: DeleteDevChannelOptions) { options = this.getOptions(options) as any; let deleteOptions = this.generateBaseRequestOptions('plugin_install', options as any); @@ -647,7 +647,7 @@ export class RokuDeploy { retainDeploymentArchive: true, incrementBuildNumber: false, failOnCompileError: true, - deleteInstalledChannel: true, + deleteDevChannel: true, packagePort: 80, remotePort: 8060, timeout: 150000, @@ -1013,7 +1013,7 @@ export interface CreateSignedPackageOptions { devId?: string; } -export interface DeleteInstalledChannelOptions { +export interface DeleteDevChannelOptions { host: string; password: string; } @@ -1029,7 +1029,7 @@ export interface DeployOptions { files?: FileEntry[]; rootDir?: string; stagingDir?: string; - deleteInstalledChannel?: boolean; + deleteDevChannel?: boolean; outFile?: string; outDir?: string; } diff --git a/src/cli.spec.ts b/src/cli.spec.ts index eaaf1fc..cd6a461 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -68,7 +68,7 @@ describe('cli', () => { }); it('Publish passes proper options', async () => { - const stub = sinon.stub(rokuDeploy, 'publish').callsFake(async () => { + const stub = sinon.stub(rokuDeploy, 'sideload').callsFake(async () => { return Promise.resolve({ message: 'Publish successful', results: {} diff --git a/src/device.spec.ts b/src/device.spec.ts index 8b6d295..df77e32 100644 --- a/src/device.spec.ts +++ b/src/device.spec.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as fsExtra from 'fs-extra'; import type { RokuDeployOptions } from './index'; import { rokuDeploy } from './index'; -import { cwd, expectPathExists, expectThrowsAsync, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; +import { cwd, expectThrowsAsync, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; import * as dedent from 'dedent'; //these tests are run against an actual roku device. These cannot be enabled when run on the CI server @@ -79,14 +79,4 @@ describe('device', function device() { ); }); }); - - describe('deployAndSignPackage', () => { - it('works', async () => { - await rokuDeploy.deleteInstalledChannel(options as any); - await rokuDeploy.rekeyDevice(options as any); - expectPathExists( - await rokuDeploy.deployAndSignPackage(options as any) - ); - }); - }); }); From 3186e880cc29edefcd3fe96f41fe8085ec9cde0d Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 4 Mar 2024 18:47:48 -0500 Subject: [PATCH 38/63] Add stagingDir, fix zip/deleteDevChannel/captureScreenshot function calls, delete Deploy function --- src/RokuDeploy.spec.ts | 9 +++-- src/RokuDeploy.ts | 21 +++++------- src/RokuDeployOptions.ts | 6 ---- src/cli.spec.ts | 33 +++---------------- src/cli.ts | 5 ++- ...lCommand.ts => DeleteDevChannelCommand.ts} | 2 +- src/commands/DeployCommand.ts | 11 ------- src/commands/ZipCommand.ts | 10 ++++++ src/commands/ZipFolderCommand.ts | 10 ------ src/commands/ZipPackageCommand.ts | 10 +++--- src/device.spec.ts | 21 ++---------- 11 files changed, 37 insertions(+), 101 deletions(-) rename src/commands/{DeleteInstalledChannelCommand.ts => DeleteDevChannelCommand.ts} (80%) delete mode 100644 src/commands/DeployCommand.ts create mode 100644 src/commands/ZipCommand.ts delete mode 100644 src/commands/ZipFolderCommand.ts diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index be55726..54c644e 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -8,7 +8,6 @@ import * as path from 'path'; import * as JSZip from 'jszip'; import * as child_process from 'child_process'; import * as glob from 'glob'; -import type { BeforeZipCallbackInfo } from './RokuDeploy'; import { RokuDeploy } from './RokuDeploy'; import * as errors from './Errors'; import { util, standardizePath as s, standardizePathPosix as sp } from './util'; @@ -2023,8 +2022,8 @@ describe('index', () => { expect(result).not.to.be.undefined; }); - it('continues with deploy if deleteInstalledChannel fails', async () => { - sinon.stub(rokuDeploy, 'deleteInstalledChannel').returns( + it('continues with deploy if deleteDevChannel fails', async () => { + sinon.stub(rokuDeploy, 'deleteDevChannel').returns( Promise.reject( new Error('failed') ) @@ -2043,7 +2042,7 @@ describe('index', () => { it('should delete installed channel if requested', async () => { fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); - const spy = sinon.spy(rokuDeploy, 'deleteInstalledChannel'); + const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); options.deleteInstalledChannel = true; mockDoPostRequest(); @@ -2060,7 +2059,7 @@ describe('index', () => { it('should not delete installed channel if not requested', async () => { fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); - const spy = sinon.spy(rokuDeploy, 'deleteInstalledChannel'); + const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); mockDoPostRequest(); await rokuDeploy.deploy({ diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 6970d0d..7e60204 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -41,17 +41,17 @@ export class RokuDeploy { await fsExtra.ensureDir(options.stagingDir); // await this.copyToStaging(options.files, options.stagingDir, options.rootDir); - if (!stagingPath) { + if (!options.stagingDir) { throw new Error('stagingPath is required'); } - if (!await fsExtra.pathExists(rootDir)) { - throw new Error(`rootDir does not exist at "${rootDir}"`); + if (!await fsExtra.pathExists(options.rootDir)) { + throw new Error(`rootDir does not exist at "${options.rootDir}"`); } - let fileObjects = await util.getFilePaths(options.files, rootDir); + let fileObjects = await util.getFilePaths(options.files, options.rootDir); //copy all of the files await Promise.all(fileObjects.map(async (fileObject) => { - let destFilePath = util.standardizePath(`${stagingPath}/${fileObject.dest}`); + let destFilePath = util.standardizePath(`${options.stagingDir}/${fileObject.dest}`); //make sure the containing folder exists await fsExtra.ensureDir(path.dirname(destFilePath)); @@ -644,6 +644,7 @@ export class RokuDeploy { let defaultOptions = { outDir: './out', outFile: 'roku-deploy', + stagingDir: `./out/.roku-deploy-staging`, retainDeploymentArchive: true, incrementBuildNumber: false, failOnCompileError: true, @@ -668,16 +669,14 @@ export class RokuDeploy { let stagingDir = finalOptions.stagingDir || finalOptions.stagingFolderPath; //stagingDir - if (stagingDir) { - finalOptions.stagingDir = path.resolve(process.cwd(), stagingDir); + if (options.stagingDir) { + finalOptions.stagingDir = path.resolve(options.cwd, options.stagingDir); } else { finalOptions.stagingDir = path.resolve( process.cwd(), util.standardizePath(`${finalOptions.outDir}/.roku-deploy-staging`) ); } - //sync the new option with the old one (for back-compat) - finalOptions.stagingFolderPath = finalOptions.stagingDir; return finalOptions; } @@ -867,10 +866,6 @@ export interface BeforeZipCallbackInfo { * Contains an associative array of the parsed values in the manifest */ manifestData: ManifestData; - /** - * @deprecated since 3.9.0. use `stagingDir` instead - */ - stagingFolderPath: string; /** * The directory where the files were staged */ diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index b05a5b4..dc1b4ff 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -49,12 +49,6 @@ export interface RokuDeployOptions { */ retainDeploymentArchive?: boolean; - /** - * The path where roku-deploy should stage all of the files right before being zipped. defaults to ${outDir}/.roku-deploy-staging - * @deprecated since 3.9.0. use `stagingDir` instead - */ - stagingFolderPath?: string; - /** * The path where roku-deploy should stage all of the files right before being zipped. defaults to ${outDir}/.roku-deploy-staging */ diff --git a/src/cli.spec.ts b/src/cli.spec.ts index cd6a461..f599044 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -9,8 +9,7 @@ import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; -import { DeployCommand } from './commands/DeployCommand'; -import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; +import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; @@ -162,36 +161,12 @@ describe('cli', () => { }); }); - it('Deploys a package', async () => { - const stub = sinon.stub(rokuDeploy, 'deploy').callsFake(async () => { - return Promise.resolve({ - message: 'Convert successful', - results: {} - }); - }); - - const command = new DeployCommand(); - await command.run({ - host: '1.2.3.4', - password: '5536', - rootDir: rootDir - }); - - expect( - stub.getCall(0).args[0] - ).to.eql({ - host: '1.2.3.4', - password: '5536', - rootDir: rootDir - }); - }); - it('Deletes an installed channel', async () => { - const stub = sinon.stub(rokuDeploy, 'deleteInstalledChannel').callsFake(async () => { + const stub = sinon.stub(rokuDeploy, 'deleteDevChannel').callsFake(async () => { return Promise.resolve({ response: {}, body: {} }); }); - const command = new DeleteInstalledChannelCommand(); + const command = new DeleteDevChannelCommand(); await command.run({ host: '1.2.3.4', password: '5536' @@ -206,7 +181,7 @@ describe('cli', () => { }); it('Takes a screenshot', async () => { - const stub = sinon.stub(rokuDeploy, 'takeScreenshot').callsFake(async () => { + const stub = sinon.stub(rokuDeploy, 'captureScreenshot').callsFake(async () => { return Promise.resolve(''); }); diff --git a/src/cli.ts b/src/cli.ts index 16cfe0a..8f4c249 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,14 +9,13 @@ import { PublishCommand } from './commands/PublishCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; -import { DeployCommand } from './commands/DeployCommand'; -import { DeleteInstalledChannelCommand } from './commands/DeleteInstalledChannelCommand'; +import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; import { GetOutputZipFilePathCommand } from './commands/GetOutputZipFilePathCommand'; import { GetOutputPkgFilePathCommand } from './commands/GetOutputPkgFilePathCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; -import { ZipFolderCommand } from './commands/ZipFolderCommand'; +import { ZipCommand } from './commands/ZipCommand'; void yargs diff --git a/src/commands/DeleteInstalledChannelCommand.ts b/src/commands/DeleteDevChannelCommand.ts similarity index 80% rename from src/commands/DeleteInstalledChannelCommand.ts rename to src/commands/DeleteDevChannelCommand.ts index b6cfebc..52dd95f 100644 --- a/src/commands/DeleteInstalledChannelCommand.ts +++ b/src/commands/DeleteDevChannelCommand.ts @@ -1,6 +1,6 @@ import { rokuDeploy } from '../index'; -export class DeleteInstalledChannelCommand { +export class DeleteDevChannelCommand { async run(args) { await rokuDeploy.deleteDevChannel({ host: args.host, diff --git a/src/commands/DeployCommand.ts b/src/commands/DeployCommand.ts deleted file mode 100644 index 5947deb..0000000 --- a/src/commands/DeployCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { rokuDeploy } from '../index'; - -export class DeployCommand { - async run(args) { - await rokuDeploy.deploy({ - host: args.host, - password: args.password, - rootDir: args.rootDir - }); - } -} diff --git a/src/commands/ZipCommand.ts b/src/commands/ZipCommand.ts new file mode 100644 index 0000000..44b0e12 --- /dev/null +++ b/src/commands/ZipCommand.ts @@ -0,0 +1,10 @@ +import { rokuDeploy } from '../index'; + +export class ZipCommand { + async run(args) { + await rokuDeploy.zip({ + stagingDir: args.stagingDir, + outDir: args.outDir + }); + } +} diff --git a/src/commands/ZipFolderCommand.ts b/src/commands/ZipFolderCommand.ts deleted file mode 100644 index edf9078..0000000 --- a/src/commands/ZipFolderCommand.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { rokuDeploy } from '../index'; - -export class ZipFolderCommand { - async run(args) { - await rokuDeploy.zipFolder( - args.srcFolder, - args.zipFilePath - ); - } -} diff --git a/src/commands/ZipPackageCommand.ts b/src/commands/ZipPackageCommand.ts index 4cd0ebf..b1463e7 100644 --- a/src/commands/ZipPackageCommand.ts +++ b/src/commands/ZipPackageCommand.ts @@ -1,10 +1,12 @@ import { rokuDeploy } from '../index'; +import { util } from '../util'; export class ZipPackageCommand { async run(args) { - await rokuDeploy.zip({ - stagingDir: args.stagingDir, - outDir: args.outDir - }); + const options = { + ...util.getOptionsFromJson(), + ...args + }; + await rokuDeploy.zip(options); } } diff --git a/src/device.spec.ts b/src/device.spec.ts index df77e32..14a5052 100644 --- a/src/device.spec.ts +++ b/src/device.spec.ts @@ -1,8 +1,7 @@ -import * as assert from 'assert'; import * as fsExtra from 'fs-extra'; import type { RokuDeployOptions } from './index'; import { rokuDeploy } from './index'; -import { cwd, expectThrowsAsync, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; +import { cwd, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; import * as dedent from 'dedent'; //these tests are run against an actual roku device. These cannot be enabled when run on the CI server @@ -62,21 +61,5 @@ describe('device', function device() { }); this.timeout(20000); - - describe('deploy', () => { - it('works', async () => { - options.retainDeploymentArchive = true; - let response = await rokuDeploy.deploy(options as any); - assert.equal(response.message, 'Successful deploy'); - }); - - it('Presents nice message for 401 unauthorized status code', async () => { - this.timeout(20000); - options.password = 'NOT_THE_PASSWORD'; - await expectThrowsAsync( - rokuDeploy.deploy(options as any), - 'Unauthorized. Please verify username and password for target Roku.' - ); - }); - }); + console.log(options); // So there are no errors about unused variable }); From 55ce426b6798384e565c01611b3a2bfa26126a97 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 4 Mar 2024 18:48:10 -0500 Subject: [PATCH 39/63] Fix commands --- src/commands/ExecCommand.ts | 32 ++++++++++--------- src/commands/KeyDownCommand.ts | 7 ++++ src/commands/KeyPressCommand.ts | 7 ++++ src/commands/KeyUpCommand.ts | 7 ++++ .../{TextCommand.ts => SendTextCommand.ts} | 2 +- 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 src/commands/KeyDownCommand.ts create mode 100644 src/commands/KeyPressCommand.ts create mode 100644 src/commands/KeyUpCommand.ts rename src/commands/{TextCommand.ts => SendTextCommand.ts} (87%) diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index bf4cfc3..9bdf9ad 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -1,24 +1,25 @@ -import { rokuDeploy } from '../index'; -import { cwd } from '../testUtils.spec'; import { util } from '../util'; +import { rokuDeploy } from '../RokuDeploy'; +import type { CloseChannelOptions, ConvertToSquashfsOptions, CreateSignedPackageOptions, DeleteDevChannelOptions, RekeyDeviceOptions, SideloadOptions } from '../RokuDeploy'; +import type { RokuDeployOptions } from '../RokuDeployOptions'; export class ExecCommand { private actions: string[]; - private configPath: string; - // eslint-disable-next-line @typescript-eslint/ban-types - private options: {}; + private options: RokuDeployOptions; - constructor(actions: string, configPath: string, ...rokuDeployOptions) { + constructor(actions: string, rokuDeployOptions: RokuDeployOptions) { this.actions = actions.split('|'); - this.configPath = configPath; this.options = rokuDeployOptions; } async run() { - //load options from json - this.options = util.getOptionsFromJson(this.options); + //Load options from json, and overwrite with cli options + this.options = { + ...util.getOptionsFromJson(this.options), + ...this.options + }; // Possibilities: // 'stage|zip' @@ -34,15 +35,16 @@ export class ExecCommand { } if (this.actions.includes('delete')) { - await rokuDeploy.deleteDevChannel(this.options); + // defaults -> config -> cli options + await rokuDeploy.deleteDevChannel(this.options as DeleteDevChannelOptions); } if (this.actions.includes('close')) { - await rokuDeploy.closeChannel(this.options); + await rokuDeploy.closeChannel(this.options as CloseChannelOptions); } if (this.actions.includes('sideload')) { - await rokuDeploy.sideload(this.options); + await rokuDeploy.sideload(this.options as SideloadOptions); } if (this.actions.includes('stage')) { @@ -50,15 +52,15 @@ export class ExecCommand { } if (this.actions.includes('rekey')) { - await rokuDeploy.rekeyDevice(this.options); + await rokuDeploy.rekeyDevice(this.options as RekeyDeviceOptions); } if (this.actions.includes('squash')) { - await rokuDeploy.convertToSquashfs(this.options); + await rokuDeploy.convertToSquashfs(this.options as ConvertToSquashfsOptions); } if (this.actions.includes('sign')) { - await rokuDeploy.createSignedPackage(this.options); + await rokuDeploy.createSignedPackage(this.options as CreateSignedPackageOptions); } diff --git a/src/commands/KeyDownCommand.ts b/src/commands/KeyDownCommand.ts new file mode 100644 index 0000000..3936e8a --- /dev/null +++ b/src/commands/KeyDownCommand.ts @@ -0,0 +1,7 @@ +import { rokuDeploy } from '../index'; + +export class KeyDownCommand { + async run(args) { + await rokuDeploy.keyDown(args.text); + } +} diff --git a/src/commands/KeyPressCommand.ts b/src/commands/KeyPressCommand.ts new file mode 100644 index 0000000..7cf1e0b --- /dev/null +++ b/src/commands/KeyPressCommand.ts @@ -0,0 +1,7 @@ +import { rokuDeploy } from '../index'; + +export class KeyPressCommand { + async run(args) { + await rokuDeploy.keyPress(args.text); + } +} diff --git a/src/commands/KeyUpCommand.ts b/src/commands/KeyUpCommand.ts new file mode 100644 index 0000000..5e7e66d --- /dev/null +++ b/src/commands/KeyUpCommand.ts @@ -0,0 +1,7 @@ +import { rokuDeploy } from '../index'; + +export class KeyUpCommand { + async run(args) { + await rokuDeploy.keyUp(args.text); + } +} diff --git a/src/commands/TextCommand.ts b/src/commands/SendTextCommand.ts similarity index 87% rename from src/commands/TextCommand.ts rename to src/commands/SendTextCommand.ts index 5408ef8..a71db16 100644 --- a/src/commands/TextCommand.ts +++ b/src/commands/SendTextCommand.ts @@ -1,6 +1,6 @@ import { rokuDeploy } from '../index'; -export class TextCommand { +export class SendTextCommand { // this.options = getDefaultArgsFromJson(this.configPath ?? `${cwd}/rokudeploy.json`);TODO async run(args) { await rokuDeploy.sendText(args.text); From 8d4e537368ffbfd80b9d0d05bd6b2839d6adbc25 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 4 Mar 2024 23:24:24 -0500 Subject: [PATCH 40/63] change defaults from json function, fix some tests --- src/RokuDeploy.spec.ts | 756 +++++++++++++++++++++++++++++++++++------ src/RokuDeploy.ts | 6 +- src/util.ts | 40 ++- 3 files changed, 683 insertions(+), 119 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 54c644e..02ef5de 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -11,7 +11,7 @@ import * as glob from 'glob'; import { RokuDeploy } from './RokuDeploy'; import * as errors from './Errors'; import { util, standardizePath as s, standardizePathPosix as sp } from './util'; -import type { RokuDeployOptions } from './RokuDeployOptions'; +import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; import { cwd, expectPathExists, expectPathNotExists, expectThrowsAsync, outDir, rootDir, stagingDir, tempDir, writeFiles } from './testUtils.spec'; import { createSandbox } from 'sinon'; import * as r from 'postman-request'; @@ -31,9 +31,6 @@ describe('index', () => { beforeEach(() => { rokuDeploy = new RokuDeploy(); - - rokuDeploy.convertToSquashfs - options = rokuDeploy.getOptions({ rootDir: rootDir, outDir: outDir, @@ -640,7 +637,7 @@ describe('index', () => { return Promise.resolve(); }); - sinon.stub(util, 'getFilePaths').returns( + sinon.stub(rokuDeploy, 'getFilePaths').returns( Promise.resolve([ { src: s`${rootDir}/source/main.brs`, @@ -726,7 +723,7 @@ describe('index', () => { process.nextTick(callback, new Error()); return {} as any; }); - return rokuDeploy.pressHomeButton({}).then(() => { + return rokuDeploy.keyPress({ ...options, host: '1.2.3.4', key: 'home' }).then(() => { assert.fail('Should have rejected the promise'); }, () => { expect(true).to.be.true; @@ -740,7 +737,7 @@ describe('index', () => { resolve(); }); }); - await rokuDeploy.pressHomeButton('1.2.3.4'); + await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', key: 'home' }); await promise; }); @@ -751,7 +748,7 @@ describe('index', () => { resolve(); }); }); - await rokuDeploy.pressHomeButton('1.2.3.4', 987); + await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', remotePort: 987, key: 'home' }); await promise; }); @@ -763,7 +760,7 @@ describe('index', () => { resolve(); }); }); - await rokuDeploy.pressHomeButton('1.2.3.4'); + await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', key: 'home' }); await promise; }); @@ -776,7 +773,7 @@ describe('index', () => { resolve(); }); }); - await rokuDeploy.pressHomeButton('1.2.3.4', 987, 1000); + await rokuDeploy.keyPress({ ...options, host: '1.2.3.4', remotePort: 987, key: 'home', timeout: 1000 }); await promise; }); }); @@ -1741,7 +1738,7 @@ describe('index', () => { let stagingDirValue = rokuDeploy.getOptions(opts).stagingDir; //getFilePaths detects the file - expect(await util.getFilePaths(['renamed_test.md'], opts.rootDir)).to.eql([{ + expect(await rokuDeploy.getFilePaths(['renamed_test.md'], opts.rootDir)).to.eql([{ src: s`${opts.rootDir}/renamed_test.md`, dest: s`renamed_test.md` }]); @@ -1786,7 +1783,7 @@ describe('index', () => { let stagingPath = rokuDeploy.getOptions(opts).stagingDir; //getFilePaths detects the file expect( - (await util.getFilePaths(opts.files, opts.rootDir)).sort((a, b) => a.src.localeCompare(b.src)) + (await rokuDeploy.getFilePaths(opts.files, opts.rootDir)).sort((a, b) => a.src.localeCompare(b.src)) ).to.eql([{ src: s`${tempDir}/mainProject/source/lib/lib.brs`, dest: s`source/lib/lib.brs` @@ -2007,72 +2004,6 @@ describe('index', () => { }); }); - describe('deploy', () => { - it('does the whole migration', async () => { - fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); - mockDoPostRequest(); - - writeFiles(rootDir, ['manifest']); - - let result = await rokuDeploy.deploy({ - rootDir: rootDir, - host: '1.2.3.4', - password: 'password' - }); - expect(result).not.to.be.undefined; - }); - - it('continues with deploy if deleteDevChannel fails', async () => { - sinon.stub(rokuDeploy, 'deleteDevChannel').returns( - Promise.reject( - new Error('failed') - ) - ); - mockDoPostRequest(); - let result = await rokuDeploy.deploy({ - host: '1.2.3.4', - password: 'password', - ...options, - //something in the previous test is locking the default output zip file. We should fix that at some point... - outDir: s`${tempDir}/test1` - }); - expect(result).not.to.be.undefined; - }); - - it('should delete installed channel if requested', async () => { - fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); - - const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); - options.deleteInstalledChannel = true; - mockDoPostRequest(); - - await rokuDeploy.deploy({ - rootDir: rootDir, - host: '1.2.3.4', - password: 'password', - deleteInstalledChannel: true - }); - - expect(spy.called).to.equal(true); - }); - - it('should not delete installed channel if not requested', async () => { - fsExtra.outputFileSync(s`${rootDir}/manifest`, ''); - - const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); - mockDoPostRequest(); - - await rokuDeploy.deploy({ - rootDir: rootDir, - host: '1.2.3.4', - password: 'password', - deleteInstalledChannel: false - }); - - expect(spy.notCalled).to.equal(true); - }); - }); - describe('deleteInstalledChannel', () => { it('attempts to delete any installed dev channel on the device', async () => { mockDoPostRequest(); @@ -2332,12 +2263,12 @@ describe('index', () => { }); }); - describe('zipFolder', () => { + describe('makeZip', () => { //this is mainly done to hit 100% coverage, but why not ensure the errors are handled properly? :D it('rejects the promise when an error occurs', async () => { //zip path doesn't exist await assertThrowsAsync(async () => { - await rokuDeploy.zipFolder('source', '.tmp/some/zip/path/that/does/not/exist'); + await rokuDeploy['makeZip']('source', '.tmp/some/zip/path/that/does/not/exist'); }); }); @@ -2362,7 +2293,7 @@ describe('index', () => { const outputZipPath = path.join(tempDir, 'output.zip'); const addedManifestLine = 'bs_libs_required=roku_ads_lib'; - await rokuDeploy.zipFolder(stageFolder, outputZipPath, (file, data) => { + await rokuDeploy['makeZip'](stageFolder, outputZipPath, (file, data) => { if (file.dest === 'manifest') { let manifestContents = data.toString(); manifestContents += addedManifestLine; @@ -2397,7 +2328,7 @@ describe('index', () => { writeFiles(stagingDir, files); const outputZipPath = path.join(tempDir, 'output.zip'); - await rokuDeploy.zipFolder(stagingDir, outputZipPath, null, ['**/*', '!**/*.map']); + await rokuDeploy['makeZip'](stagingDir, outputZipPath, null, ['**/*', '!**/*.map']); const data = fsExtra.readFileSync(outputZipPath); const zip = await JSZip.loadAsync(data); @@ -2415,6 +2346,648 @@ describe('index', () => { }); }); + describe('getFilePaths', () => { + const otherProjectName = 'otherProject'; + const otherProjectDir = sp`${rootDir}/../${otherProjectName}`; + //create baseline project structure + beforeEach(() => { + rokuDeploy = new RokuDeploy(); + options = rokuDeploy.getOptions({}); + fsExtra.ensureDirSync(`${rootDir}/components/emptyFolder`); + writeFiles(rootDir, [ + `manifest`, + `source/main.brs`, + `source/lib.brs`, + `components/component1.xml`, + `components/component1.brs`, + `components/screen1/screen1.xml`, + `components/screen1/screen1.brs` + ]); + }); + + async function getFilePaths(files: FileEntry[], rootDirOverride = rootDir) { + return (await rokuDeploy.getFilePaths(files, rootDirOverride)) + .sort((a, b) => a.src.localeCompare(b.src)); + } + + describe('top-level-patterns', () => { + it('excludes a file that is negated', async () => { + expect(await getFilePaths([ + 'source/**/*', + '!source/main.brs' + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }]); + }); + + it('excludes file from non-rootdir top-level pattern', async () => { + writeFiles(rootDir, ['../externalDir/source/main.brs']); + expect(await getFilePaths([ + '../externalDir/**/*', + '!../externalDir/**/*' + ])).to.eql([]); + }); + + it('throws when using top-level string referencing file outside the root dir', async () => { + writeFiles(rootDir, [`../source/main.brs`]); + await expectThrowsAsync(async () => { + await getFilePaths([ + '../source/**/*' + ]); + }, 'Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); + }); + + it('works for brighterscript files', async () => { + writeFiles(rootDir, ['src/source/main.bs']); + expect(await getFilePaths([ + 'manifest', + 'source/**/*.bs' + ], s`${rootDir}/src`)).to.eql([{ + src: s`${rootDir}/src/source/main.bs`, + dest: s`source/main.bs` + }]); + }); + + it('works for root-level double star in top-level pattern', async () => { + expect(await getFilePaths([ + '**/*' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, + { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, + { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, + { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, + { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, + { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('works for multile entries', async () => { + expect(await getFilePaths([ + 'source/**/*', + 'components/**/*', + 'manifest' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('copies top-level-string single-star globs', async () => { + writeFiles(rootDir, [ + 'source/lib.brs', + 'source/main.brs' + ]); + expect(await getFilePaths([ + 'source/*.brs' + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('works for double-star globs', async () => { + expect(await getFilePaths([ + '**/*.brs' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('copies subdir-level relative double-star globs', async () => { + expect(await getFilePaths([ + 'components/**/*.brs' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }]); + }); + + it('Finds folder using square brackets glob pattern', async () => { + fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); + expect(await getFilePaths([ + '[test]/*' + ], + rootDir + )).to.eql([{ + src: s`${rootDir}/e/file.brs`, + dest: s`e/file.brs` + }]); + }); + + it('Finds folder with escaped square brackets glob pattern as name', async () => { + fsExtra.outputFileSync(`${rootDir}/[test]/file.brs`, ''); + fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); + expect(await getFilePaths([ + '\\[test\\]/*' + ], + rootDir + )).to.eql([{ + src: s`${rootDir}/[test]/file.brs`, + dest: s`[test]/file.brs` + }]); + }); + + it('throws exception when top-level strings reference files not under rootDir', async () => { + writeFiles(otherProjectDir, [ + 'manifest' + ]); + await expectThrowsAsync( + getFilePaths([ + `../${otherProjectName}/**/*` + ]) + ); + }); + + it('applies negated patterns', async () => { + expect(await getFilePaths([ + //include all components + 'components/**/*.brs', + //exclude all xml files + '!components/**/*.xml', + //re-include a specific xml file + 'components/screen1/screen1.xml' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }]); + }); + + it('handles negated multi-globs', async () => { + expect((await getFilePaths([ + 'components/**/*', + '!components/screen1/**/*' + ])).map(x => x.dest)).to.eql([ + s`components/component1.brs`, + s`components/component1.xml` + ]); + }); + + it('allows negating paths outside rootDir without requiring src;dest; syntax', async () => { + fsExtra.outputFileSync(`${rootDir}/../externalLib/source/lib.brs`, ''); + const filePaths = await getFilePaths([ + 'source/**/*', + { src: '../externalLib/**/*', dest: 'source' }, + '!../externalLib/source/**/*' + ], rootDir); + expect( + filePaths.map(x => s`${x.src}`).sort() + ).to.eql([ + s`${rootDir}/source/lib.brs`, + s`${rootDir}/source/main.brs` + ]); + }); + + it('applies multi-glob paths relative to rootDir', async () => { + expect(await getFilePaths([ + 'manifest', + 'source/**/*', + 'components/**/*', + '!components/scenes/**/*' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('ignores non-glob folder paths', async () => { + expect(await getFilePaths([ + //this is the folder called "components" + 'components' + ])).to.eql([]); //there should be no matches because rokudeploy ignores folders + }); + + }); + + describe('{src;dest} objects', () => { + it('excludes a file that is negated in src;dest;', async () => { + expect(await getFilePaths([ + 'source/**/*', + { + src: '!source/main.brs' + } + ])).to.eql([{ + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }]); + }); + + it('works for root-level double star in {src;dest} object', async () => { + expect(await getFilePaths([{ + src: '**/*', + dest: '' + } + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, + { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, + { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }, + { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }, + { + src: s`${rootDir}/source/lib.brs`, + dest: s`source/lib.brs` + }, + { + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('uses the root of staging folder for dest when not specified with star star', async () => { + writeFiles(otherProjectDir, [ + 'components/component1/subComponent/screen.brs', + 'manifest', + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `${otherProjectDir}/**/*` + }])).to.eql([{ + src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, + dest: s`components/component1/subComponent/screen.brs` + }, { + src: s`${otherProjectDir}/manifest`, + dest: s`manifest` + }, { + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`source/thirdPartyLib.brs` + }]); + }); + + it('copies absolute path files to specified dest', async () => { + writeFiles(otherProjectDir, [ + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `${otherProjectDir}/source/thirdPartyLib.brs`, + dest: 'lib/thirdPartyLib.brs' + }])).to.eql([{ + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`lib/thirdPartyLib.brs` + }]); + }); + + it('copies relative path files to specified dest', async () => { + const rootDirName = path.basename(rootDir); + writeFiles(rootDir, [ + 'source/main.brs' + ]); + expect(await getFilePaths([{ + src: `../${rootDirName}/source/main.brs`, + dest: 'source/main.brs' + }])).to.eql([{ + src: s`${rootDir}/source/main.brs`, + dest: s`source/main.brs` + }]); + }); + + it('maintains relative path after **', async () => { + writeFiles(otherProjectDir, [ + 'components/component1/subComponent/screen.brs', + 'manifest', + 'source/thirdPartyLib.brs' + ]); + expect(await getFilePaths([{ + src: `../otherProject/**/*`, + dest: 'outFolder/' + }])).to.eql([{ + src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, + dest: s`outFolder/components/component1/subComponent/screen.brs` + }, { + src: s`${otherProjectDir}/manifest`, + dest: s`outFolder/manifest` + }, { + src: s`${otherProjectDir}/source/thirdPartyLib.brs`, + dest: s`outFolder/source/thirdPartyLib.brs` + }]); + }); + + it('works for other globs', async () => { + expect(await getFilePaths([{ + src: `components/screen1/*creen1.brs`, + dest: s`/source` + }])).to.eql([{ + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`source/screen1.brs` + }]); + }); + + it('applies negated patterns', async () => { + writeFiles(rootDir, [ + 'components/component1.brs', + 'components/component1.xml', + 'components/screen1/screen1.brs', + 'components/screen1/screen1.xml' + ]); + expect(await getFilePaths([ + //include all component brs files + 'components/**/*.brs', + //exclude all xml files + '!components/**/*.xml', + //re-include a specific xml file + 'components/screen1/screen1.xml' + ])).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.brs`, + dest: s`components/screen1/screen1.brs` + }, { + src: s`${rootDir}/components/screen1/screen1.xml`, + dest: s`components/screen1/screen1.xml` + }]); + }); + }); + + it('converts relative rootDir path to absolute', async () => { + let stub = sinon.stub(rokuDeploy, 'getOptions').callThrough(); + await getFilePaths([ + 'source/main.brs' + ], './rootDir'); + expect(stub.callCount).to.be.greaterThan(0); + expect(stub.getCall(0).args[0].rootDir).to.eql('./rootDir'); + expect(stub.getCall(0).returnValue.rootDir).to.eql(s`${cwd}/rootDir`); + }); + + it('works when using a different current working directory than rootDir', async () => { + writeFiles(rootDir, [ + 'manifest', + 'images/splash_hd.jpg' + ]); + //sanity check, make sure it works without fiddling with cwd intact + let paths = await getFilePaths([ + 'manifest', + 'images/splash_hd.jpg' + ]); + + expect(paths).to.eql([{ + src: s`${rootDir}/images/splash_hd.jpg`, + dest: s`images/splash_hd.jpg` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }]); + + //change the working directory and verify everything still works + + let wrongCwd = path.dirname(path.resolve(options.rootDir)); + process.chdir(wrongCwd); + + paths = await getFilePaths([ + 'manifest', + 'images/splash_hd.jpg' + ]); + + expect(paths).to.eql([{ + src: s`${rootDir}/images/splash_hd.jpg`, + dest: s`images/splash_hd.jpg` + }, { + src: s`${rootDir}/manifest`, + dest: s`manifest` + }]); + }); + + it('supports absolute paths from outside of the rootDir', async () => { + //dest not specified + expect(await getFilePaths([{ + src: sp`${cwd}/README.md` + }], options.rootDir)).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`README.md` + }]); + + //dest specified + expect(await getFilePaths([{ + src: sp`${cwd}/README.md`, + dest: 'docs/README.md' + }], options.rootDir)).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`docs/README.md` + }]); + + let paths: any[]; + + paths = await getFilePaths([{ + src: sp`${cwd}/README.md`, + dest: s`docs/README.md` + }], outDir); + + expect(paths).to.eql([{ + src: s`${cwd}/README.md`, + dest: s`docs/README.md` + }]); + + //top-level string paths pointing to files outside the root should thrown an exception + await expectThrowsAsync(async () => { + paths = await getFilePaths([ + sp`${cwd}/README.md` + ], outDir); + }); + }); + + it('supports relative paths that grab files from outside of the rootDir', async () => { + writeFiles(`${rootDir}/../`, [ + 'README.md' + ]); + expect( + await getFilePaths([{ + src: sp`../README.md` + }], rootDir) + ).to.eql([{ + src: s`${rootDir}/../README.md`, + dest: s`README.md` + }]); + + expect( + await getFilePaths([{ + src: sp`../README.md`, + dest: 'docs/README.md' + }], rootDir) + ).to.eql([{ + src: s`${rootDir}/../README.md`, + dest: s`docs/README.md` + }]); + }); + + it('should throw exception because we cannot have top-level string paths pointed to files outside the root', async () => { + writeFiles(rootDir, [ + '../README.md' + ]); + await expectThrowsAsync( + getFilePaths([ + path.posix.join('..', 'README.md') + ], outDir) + ); + }); + + it('supports overriding paths', async () => { + let paths = await getFilePaths([{ + src: sp`${rootDir}/components/component1.brs`, + dest: 'comp1.brs' + }, { + src: sp`${rootDir}/components/screen1/screen1.brs`, + dest: 'comp1.brs' + }], rootDir); + expect(paths).to.be.lengthOf(1); + expect(s`${paths[0].src}`).to.equal(s`${rootDir}/components/screen1/screen1.brs`); + }); + + it('supports overriding paths from outside the root dir', async () => { + let thisRootDir = s`${tempDir}/tempTestOverrides/src`; + try { + + fsExtra.ensureDirSync(s`${thisRootDir}/source`); + fsExtra.ensureDirSync(s`${thisRootDir}/components`); + fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); + + fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); + fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); + + let files = [ + '**/*.xml', + '**/*.brs', + { + src: '../.tmp/MainScene.brs', + dest: 'components/MainScene.brs' + } + ]; + let paths = await getFilePaths(files, thisRootDir); + + //the MainScene.brs file from source should NOT be included + let mainSceneEntries = paths.filter(x => s`${x.dest}` === s`components/MainScene.brs`); + expect( + mainSceneEntries, + `Should only be one files entry for 'components/MainScene.brs'` + ).to.be.lengthOf(1); + expect(s`${mainSceneEntries[0].src}`).to.eql(s`${thisRootDir}/../.tmp/MainScene.brs`); + } finally { + //clean up + await fsExtra.remove(s`${thisRootDir}/../`); + } + }); + + it('maintains original file path', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + await getFilePaths([ + 'components/CustomButton.brs' + ], rootDir) + ).to.eql([{ + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }]); + }); + + it('correctly assumes file path if not given', async () => { + fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); + expect( + (await getFilePaths([ + { src: 'components/*' } + ], rootDir)).sort((a, b) => a.src.localeCompare(b.src)) + ).to.eql([{ + src: s`${rootDir}/components/component1.brs`, + dest: s`components/component1.brs` + }, { + src: s`${rootDir}/components/component1.xml`, + dest: s`components/component1.xml` + }, { + src: s`${rootDir}/components/CustomButton.brs`, + dest: s`components/CustomButton.brs` + }]); + }); + }); + describe('parseManifest', () => { it('correctly parses valid manifest', async () => { fsExtra.outputFileSync(`${rootDir}/manifest`, `title=AwesomeApp`); @@ -2456,7 +3029,7 @@ describe('index', () => { describe('stringifyManifest', () => { it('correctly converts back to a valid manifest when lineNumber and keyIndexes are provided', () => { expect( - rokuDeploy.stringifyManifest( + rokuDeploy['stringifyManifest']( rokuDeploy['parseManifestFromString']('major_version=3\nminor_version=4') ) ).to.equal( @@ -2469,7 +3042,7 @@ describe('index', () => { delete parsed.keyIndexes; delete parsed.lineCount; let outputParsedManifest = rokuDeploy['parseManifestFromString']( - rokuDeploy.stringifyManifest(parsed) + rokuDeploy['stringifyManifest'](parsed) ); expect(outputParsedManifest.title).to.equal('App'); expect(outputParsedManifest.major_version).to.equal('3'); @@ -2503,21 +3076,6 @@ describe('index', () => { }); describe('getOptions', () => { - it('supports deprecated stagingFolderPath option', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path' }).stagingDir - ).to.eql(s`${cwd}/staging-folder-path`); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path', stagingDir: 'staging-dir' }).stagingDir - ).to.eql(s`${cwd}/staging-dir`); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path' }).stagingFolderPath - ).to.eql(s`${cwd}/staging-folder-path`); - }); - it('calling with no parameters works', () => { sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { return false; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 7e60204..1185a90 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -7,8 +7,7 @@ const request = r as typeof requestType; import * as JSZip from 'jszip'; import * as errors from './Errors'; import * as xml2js from 'xml2js'; -import type { ParseError } from 'jsonc-parser'; -import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; +import { parse as parseJsonc } from 'jsonc-parser'; import { util } from './util'; import type { RokuDeployOptions, FileEntry } from './RokuDeployOptions'; import { Logger, LogLevel } from './Logger'; @@ -24,9 +23,6 @@ export class RokuDeploy { private logger: Logger; - //this should just - // public screenshotDir = path.join(tempDir, '/roku-deploy/screenshots/'); - /** * Copies all of the referenced files to the staging folder * @param options diff --git a/src/util.ts b/src/util.ts index 7c1846d..9254027 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,6 +9,7 @@ import type { FileEntry } from './RokuDeployOptions'; import type { StandardizedFileEntry } from './RokuDeploy'; import * as isGlob from 'is-glob'; import * as picomatch from 'picomatch'; +import { ParseError, parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; export class Util { /** @@ -485,24 +486,33 @@ export class Util { /** * A function to fill in any missing arguments with JSON values + * Only run when CLI commands are used */ - public getOptionsFromJson(defaultArgs) {//TODO: The original function handled parse errors, but this one doesn't - let args = { ...defaultArgs }; - const fileNames = ['rokudeploy.json', 'bsconfig.json']; - - for (const fileName of fileNames) { - if (fsExtra.existsSync(fileName)) { - const sourcePath = path.join(defaultArgs.rootDir, fileName); - const rokuDeployArgs = JSON.parse(fsExtra.readFileSync(sourcePath, 'utf8')); - // args = Object.assign(rokudeployArgs ?? {}, args); - args = Object.assign(rokuDeployArgs, args); - break; - } + public getOptionsFromJson(options?: { cwd?: string }) { + let fileOptions: RokuDeployOptions = {}; + const cwd = options?.cwd ?? process.cwd(); + const configPath = path.join(cwd, 'rokudeploy.json'); + + 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 args; + return fileOptions; } - } export let util = new Util(); From 1d75b10f7c7014fde0eb63c34e53d4b9ae8bb7f1 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 4 Mar 2024 23:27:42 -0500 Subject: [PATCH 41/63] fix index, fix makezip, getfilepaths, sideload --- src/RokuDeploy.ts | 150 ++++++++++++++++++++++++++-------------------- src/index.ts | 5 -- src/util.ts | 48 +-------------- 3 files changed, 87 insertions(+), 116 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 1185a90..a6c81f7 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -14,6 +14,7 @@ import { Logger, LogLevel } from './Logger'; import * as dayjs from 'dayjs'; import * as lodash from 'lodash'; import type { DeviceInfo, DeviceInfoRaw } from './DeviceInfo'; +import * as tempDir from 'temp-dir'; export class RokuDeploy { @@ -44,7 +45,7 @@ export class RokuDeploy { throw new Error(`rootDir does not exist at "${options.rootDir}"`); } - let fileObjects = await util.getFilePaths(options.files, options.rootDir); + let fileObjects = await this.getFilePaths(options.files, options.rootDir); //copy all of the files await Promise.all(fileObjects.map(async (fileObject) => { let destFilePath = util.standardizePath(`${options.stagingDir}/${fileObject.dest}`); @@ -82,7 +83,7 @@ export class RokuDeploy { } //create a zip of the staging folder - await this.__makeZip(options.stagingDir, zipFilePath); + await this.makeZip(options.stagingDir, zipFilePath); } /** @@ -92,8 +93,8 @@ export class RokuDeploy { * @param preZipCallback a function to call right before every file gets added to the zip * @param files a files array used to filter the files from `srcFolder` */ - private async __makeZip(srcFolder: string, zipFilePath: string, preFileZipCallback?: (file: StandardizedFileEntry, data: Buffer) => Buffer, files: FileEntry[] = ['**/*']) { - const filePaths = await util.getFilePaths(files, srcFolder); + private async makeZip(srcFolder: string, zipFilePath: string, preFileZipCallback?: (file: StandardizedFileEntry, data: Buffer) => Buffer, files: FileEntry[] = ['**/*']) { + const filePaths = await this.getFilePaths(files, srcFolder); const zip = new JSZip(); // Allows us to wait until all are done before we build the zip @@ -122,6 +123,49 @@ export class RokuDeploy { return fsExtra.writeFile(zipFilePath, content); } + /** + * Get all file paths for the specified options + * @param files + * @param rootFolderPath - the absolute path to the root dir where relative files entries are relative to + */ + public async getFilePaths(files: FileEntry[], rootDir: string): Promise { + //if the rootDir isn't absolute, convert it to absolute using the standard options flow + if (path.isAbsolute(rootDir) === false) { + rootDir = this.getOptions({ rootDir: rootDir }).rootDir; + } + const entries = util.normalizeFilesArray(files); + const srcPathsByIndex = await util.globAllByIndex( + entries.map(x => { + return typeof x === 'string' ? x : x.src; + }), + rootDir + ); + + /** + * Result indexed by the dest path + */ + let result = new Map(); + + //compute `dest` path for every file + for (let i = 0; i < srcPathsByIndex.length; i++) { + const srcPaths = srcPathsByIndex[i]; + const entry = entries[i]; + if (srcPaths) { + for (let srcPath of srcPaths) { + srcPath = util.standardizePath(srcPath); + + const dest = util.computeFileDestPath(srcPath, entry, rootDir); + //the last file with this `dest` will win, so just replace any existing entry with this one. + result.set(dest, { + src: srcPath, + dest: dest + }); + } + } + } + return [...result.values()]; + } + private generateBaseRequestOptions(requestPath: string, options: BaseRequestOptions, formData = {} as T): requestType.OptionsWithUrl { options = this.getOptions(options) as any; let url = `http://${options.host}:${options.packagePort}/${requestPath}`; @@ -188,18 +232,25 @@ export class RokuDeploy { }, false); } - public async closeChannel(options: CloseAppOptions) { - //TODO - - //if supports ecp close-app, then do that (twice so it kills instant resume) - //else, send home press + public async closeChannel(options: CloseChannelOptions) { + // TODO: After 13.0 releases, add check for ECP close-app support, and use that if available + await this.sendKeyEvent({ + ...options, + action: 'keypress', + key: 'home' + }); + return this.sendKeyEvent({ + ...options, + action: 'keypress', + key: 'home' + }); } /** * Publish a pre-existing packaged zip file to a remote Roku. * @param options */ - public async sideload(options: PublishOptions): Promise<{ message: string; results: any }> { + public async sideload(options: SideloadOptions): Promise<{ message: string; results: any }> { options = this.getOptions(options) as any; if (!options.host) { throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); @@ -209,7 +260,7 @@ export class RokuDeploy { let zipFilePath = this.getOutputZipFilePath(options as any); - if (options.deleteInstalledChannel) { + if (options.deleteDevChannel) { try { await this.deleteDevChannel(options); } catch (e) { @@ -567,8 +618,8 @@ export class RokuDeploy { * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ public async captureScreenshot(options: TakeScreenshotOptions) { - options.outDir = options.outDir ?? this.screenshotDir; - options.outFile = options.outFile ?? `screenshot-${dayjs().format('YYYY-MM-DD-HH.mm.ss.SSS')}`; + options = this.getOptions(options); + options.screenshotFile = options.screenshotFile ?? `screenshot-${dayjs().format('YYYY-MM-DD-HH.mm.ss.SSS')}`; let saveFilePath: string; // Ask for the device to make an image @@ -584,13 +635,13 @@ export class RokuDeploy { const [_, imageUrlOnDevice, imageExt] = /["'](pkgs\/dev(\.jpg|\.png)\?.+?)['"]/gi.exec(createScreenshotResult.body) ?? []; if (imageUrlOnDevice) { - saveFilePath = util.standardizePath(path.join(options.outDir, options.outFile + imageExt)); + saveFilePath = util.standardizePath(path.join(options.screenshotDir, options.screenshotFile + imageExt)); await this.getToFile( this.generateBaseRequestOptions(imageUrlOnDevice, options), saveFilePath ); } else { - throw new Error('No screen shot url returned from device'); + throw new Error('No screenshot url returned from device'); } return saveFilePath; } @@ -631,13 +682,12 @@ export class RokuDeploy { } /** - * Get an options with all overridden vaues, and then defaults for missing values + * Get an options with all overridden values, and then defaults for missing values * @param options */ - private __getOptions(options: RokuDeployOptions = {}) { - let fileOptions: RokuDeployOptions = {}; - + public getOptions(options: T & RokuDeployOptions = {} as any): RokuDeployOptions & T { let defaultOptions = { + cwd: process.cwd(), outDir: './out', outFile: 'roku-deploy', stagingDir: `./out/.roku-deploy-staging`, @@ -651,25 +701,25 @@ export class RokuDeploy { rootDir: './', files: [...DefaultFiles], username: 'rokudev', - logLevel: LogLevel.log + logLevel: LogLevel.log, + screenshotDir: path.join(tempDir, '/roku-deploy/screenshots/') }; - //override the defaults with any found or provided options - let finalOptions = { ...defaultOptions, ...fileOptions, ...options }; - this.logger.logLevel = finalOptions.logLevel; + // Fill in default options for any missing values + let finalOptions = { ...defaultOptions, ...options }; + finalOptions.cwd ??= process.cwd(); + this.logger.logLevel = finalOptions.logLevel; //TODO: Handle logging differently //fully resolve the folder paths - finalOptions.rootDir = path.resolve(process.cwd(), finalOptions.rootDir); - finalOptions.outDir = path.resolve(process.cwd(), finalOptions.outDir); - - let stagingDir = finalOptions.stagingDir || finalOptions.stagingFolderPath; + finalOptions.rootDir = path.resolve(options.cwd, finalOptions.rootDir); + finalOptions.outDir = path.resolve(options.cwd, finalOptions.outDir); //stagingDir if (options.stagingDir) { finalOptions.stagingDir = path.resolve(options.cwd, options.stagingDir); } else { finalOptions.stagingDir = path.resolve( - process.cwd(), + options.cwd, util.standardizePath(`${finalOptions.outDir}/.roku-deploy-staging`) ); } @@ -782,7 +832,6 @@ export class RokuDeploy { } /** - * TODO we might delete this one. let chris think about it. ;) * @param options * @returns */ @@ -791,9 +840,6 @@ export class RokuDeploy { return deviceInfo['keyed-developer-id']; } - /** - * TODO move these manifest functions to a util somewhere - */ private async parseManifest(manifestPath: string): Promise { if (!await fsExtra.pathExists(manifestPath)) { throw new Error(manifestPath + ' does not exist'); @@ -803,9 +849,6 @@ export class RokuDeploy { return this.parseManifestFromString(manifestContents); } - /** - * TODO move these manifest functions to a util somewhere - */ private parseManifestFromString(manifestContents: string): ManifestData { let manifestLines = manifestContents.split('\n'); let manifestData: ManifestData = {}; @@ -822,33 +865,6 @@ export class RokuDeploy { return manifestData; } - - /** - * TODO move these manifest functions to a util somewhere - */ - public stringifyManifest(manifestData: ManifestData): string { - let output = []; - - if (manifestData.keyIndexes && manifestData.lineCount) { - output.fill('', 0, manifestData.lineCount); - - let key; - for (key in manifestData) { - if (key === 'lineCount' || key === 'keyIndexes') { - continue; - } - - let index = manifestData.keyIndexes[key]; - output[index] = `${key}=${manifestData[key]}`; - } - } else { - output = Object.keys(manifestData).map((key) => { - return `${key}=${manifestData[key]}`; - }); - } - - return output.join('\n'); - } } export interface ManifestData { @@ -919,13 +935,13 @@ export interface TakeScreenshotOptions { * A full path to the folder where the screenshots should be saved. * Will use the OS temp directory by default */ - outDir?: string; + screenshotDir?: string; /** * The base filename the image file should be given (excluding the extension) * The default format looks something like this: screenshot-YYYY-MM-DD-HH.mm.ss.SSS. */ - outFile?: string; + screenshotFile?: string; } export interface GetDeviceInfoOptions { @@ -960,7 +976,7 @@ export interface ZipPackageOptions { outDir?: string; } -export interface PublishOptions { +export interface SideloadOptions { host: string; password: string; remoteDebug?: boolean; @@ -969,6 +985,7 @@ export interface PublishOptions { retainDeploymentArchive?: boolean; outDir?: string; outFile?: string; + deleteDevChannel?: boolean; } export interface BaseRequestOptions { @@ -1041,3 +1058,6 @@ export interface GetDevIdOptions { */ timeout?: number; } + +//create a new static instance of RokuDeploy, and export those functions for backwards compatibility +export const rokuDeploy = new RokuDeploy(); diff --git a/src/index.ts b/src/index.ts index 8c00ae0..2fc0ec7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,6 @@ -import { RokuDeploy } from './RokuDeploy'; - //export everything from the RokuDeploy file export * from './RokuDeploy'; export * from './util'; export * from './RokuDeployOptions'; export * from './Errors'; export * from './DeviceInfo'; - -//create a new static instance of RokuDeploy, and export those functions for backwards compatibility -export const rokuDeploy = new RokuDeploy(); diff --git a/src/util.ts b/src/util.ts index 9254027..2895b06 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,7 +5,7 @@ import * as dns from 'dns'; import * as micromatch from 'micromatch'; // eslint-disable-next-line @typescript-eslint/no-require-imports import fastGlob = require('fast-glob'); -import type { FileEntry } from './RokuDeployOptions'; +import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; import type { StandardizedFileEntry } from './RokuDeploy'; import * as isGlob from 'is-glob'; import * as picomatch from 'picomatch'; @@ -283,50 +283,6 @@ export class Util { return result; } - - /** - * Get all file paths for the specified options - * @param files - * @param rootFolderPath - the absolute path to the root dir where relative files entries are relative to - */ - public async getFilePaths(files: FileEntry[], rootDir: string): Promise { - //if the rootDir isn't absolute, convert it to absolute using the standard options flow - if (path.isAbsolute(rootDir) === false) { - rootDir = rokuDeploy.getOptions({ rootDir: rootDir }).rootDir; //TODO: This moved from rokudeploy to here, but if we need to get options how do we fix this? - } - const entries = this.normalizeFilesArray(files); - const srcPathsByIndex = await util.globAllByIndex( - entries.map(x => { - return typeof x === 'string' ? x : x.src; - }), - rootDir - ); - - /** - * Result indexed by the dest path - */ - let result = new Map(); - - //compute `dest` path for every file - for (let i = 0; i < srcPathsByIndex.length; i++) { - const srcPaths = srcPathsByIndex[i]; - const entry = entries[i]; - if (srcPaths) { - for (let srcPath of srcPaths) { - srcPath = util.standardizePath(srcPath); - - const dest = this.computeFileDestPath(srcPath, entry, rootDir); - //the last file with this `dest` will win, so just replace any existing entry with this one. - result.set(dest, { - src: srcPath, - dest: dest - }); - } - } - } - return [...result.values()]; - } - /** * Given a full path to a file, determine its dest path * @param srcPath the absolute path to the file. This MUST be a file path, and it is not verified to exist on the filesystem @@ -388,7 +344,7 @@ export class Util { * @param pattern the glob pattern originally used to find this file * @param rootDir absolute normalized path to the rootDir */ - private computeFileDestPath(srcPath: string, entry: string | StandardizedFileEntry, rootDir: string) { + public computeFileDestPath(srcPath: string, entry: string | StandardizedFileEntry, rootDir: string) { let result: string; let globstarIdx: number; //files under rootDir with no specified dest From 48d07d901b8c70baba2185b68d62db2388b0ea1f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 5 Mar 2024 12:53:51 -0500 Subject: [PATCH 42/63] Add options from json ability to each command to each command --- src/commands/ConvertToSquashfsCommand.ts | 11 +- src/commands/CreateSignedPackageCommand.ts | 13 +- src/commands/DeleteDevChannelCommand.ts | 11 +- src/commands/ExecCommand.ts | 5 - src/commands/GetDevIdCommand.ts | 10 +- src/commands/GetDeviceInfoCommand.ts | 8 +- src/commands/GetOutputPkgFilePathCommand.ts | 11 +- src/commands/GetOutputZipFilePathCommand.ts | 11 +- src/commands/KeyDownCommand.ts | 8 +- src/commands/KeyPressCommand.ts | 8 +- src/commands/KeyUpCommand.ts | 8 +- src/commands/PrepublishCommand.ts | 11 +- src/commands/PublishCommand.ts | 13 +- src/commands/RekeyDeviceCommand.ts | 15 +- src/commands/SendTextCommand.ts | 9 +- src/commands/TakeScreenshotCommand.ts | 11 +- src/commands/ZipCommand.ts | 11 +- src/util.spec.ts | 690 +------------------- 18 files changed, 111 insertions(+), 753 deletions(-) diff --git a/src/commands/ConvertToSquashfsCommand.ts b/src/commands/ConvertToSquashfsCommand.ts index b1e1e7b..79ebf6e 100644 --- a/src/commands/ConvertToSquashfsCommand.ts +++ b/src/commands/ConvertToSquashfsCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class ConvertToSquashfsCommand { async run(args) { - await rokuDeploy.convertToSquashfs({ - host: args.host, - password: args.password - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.convertToSquashfs(options); } } diff --git a/src/commands/CreateSignedPackageCommand.ts b/src/commands/CreateSignedPackageCommand.ts index 8488650..904a9fe 100644 --- a/src/commands/CreateSignedPackageCommand.ts +++ b/src/commands/CreateSignedPackageCommand.ts @@ -1,12 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class CreateSignedPackageCommand { async run(args) { - await rokuDeploy.createSignedPackage({ - host: args.host, - password: args.password, - signingPassword: args.signingPassword, - stagingDir: args.stagingDir - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.createSignedPackage(options); } } diff --git a/src/commands/DeleteDevChannelCommand.ts b/src/commands/DeleteDevChannelCommand.ts index 52dd95f..5bfa269 100644 --- a/src/commands/DeleteDevChannelCommand.ts +++ b/src/commands/DeleteDevChannelCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class DeleteDevChannelCommand { async run(args) { - await rokuDeploy.deleteDevChannel({ - host: args.host, - password: args.password - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.deleteDevChannel(options); } } diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index 9bdf9ad..5a56b44 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -21,11 +21,6 @@ export class ExecCommand { ...this.options }; - // Possibilities: - // 'stage|zip' - // 'stage|zip|delete|close|sideload' - // 'close|rekey|stage|zip|delete|close|sideload|squash|sign' - if (this.actions.includes('stage')) { await rokuDeploy.stage(this.options); } diff --git a/src/commands/GetDevIdCommand.ts b/src/commands/GetDevIdCommand.ts index 72ccde8..c54c8e6 100644 --- a/src/commands/GetDevIdCommand.ts +++ b/src/commands/GetDevIdCommand.ts @@ -1,9 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class GetDevIdCommand { async run(args) { - await rokuDeploy.getDevId({ - host: args.host - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.getDevId(options); } } diff --git a/src/commands/GetDeviceInfoCommand.ts b/src/commands/GetDeviceInfoCommand.ts index 1bf10b3..3255fe3 100644 --- a/src/commands/GetDeviceInfoCommand.ts +++ b/src/commands/GetDeviceInfoCommand.ts @@ -3,9 +3,11 @@ import { util } from '../util'; export class GetDeviceInfoCommand { async run(args) { - const outputPath = await rokuDeploy.getDeviceInfo({ - host: args.host - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + const outputPath = await rokuDeploy.getDeviceInfo(options); console.log(util.objectToTableString(outputPath)); } } diff --git a/src/commands/GetOutputPkgFilePathCommand.ts b/src/commands/GetOutputPkgFilePathCommand.ts index 0861d1e..3bfc2d0 100644 --- a/src/commands/GetOutputPkgFilePathCommand.ts +++ b/src/commands/GetOutputPkgFilePathCommand.ts @@ -1,12 +1,13 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class GetOutputPkgFilePathCommand { run(args) { + let options = { + ...util.getOptionsFromJson(args), + ...args + }; // eslint-disable-next-line @typescript-eslint/dot-notation - const outputPath = rokuDeploy['getOutputPkgPath']({ - outFile: args.outFile, - outDir: args.outDir - }); + const outputPath = rokuDeploy['getOutputPkgPath'](options); //TODO fix this? console.log(outputPath); } } diff --git a/src/commands/GetOutputZipFilePathCommand.ts b/src/commands/GetOutputZipFilePathCommand.ts index bd9d7de..c5761c0 100644 --- a/src/commands/GetOutputZipFilePathCommand.ts +++ b/src/commands/GetOutputZipFilePathCommand.ts @@ -1,12 +1,13 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class GetOutputZipFilePathCommand { run(args) { + let options = { + ...util.getOptionsFromJson(args), + ...args + }; // eslint-disable-next-line @typescript-eslint/dot-notation - const outputPath = rokuDeploy['getOutputZipFilePath']({ - outFile: args.outFile, - outDir: args.outDir - }); + const outputPath = rokuDeploy['getOutputZipFilePath'](options); console.log(outputPath); } } diff --git a/src/commands/KeyDownCommand.ts b/src/commands/KeyDownCommand.ts index 3936e8a..ae05b91 100644 --- a/src/commands/KeyDownCommand.ts +++ b/src/commands/KeyDownCommand.ts @@ -1,7 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class KeyDownCommand { async run(args) { - await rokuDeploy.keyDown(args.text); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.keyDown(options); } } diff --git a/src/commands/KeyPressCommand.ts b/src/commands/KeyPressCommand.ts index 7cf1e0b..4554df7 100644 --- a/src/commands/KeyPressCommand.ts +++ b/src/commands/KeyPressCommand.ts @@ -1,7 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class KeyPressCommand { async run(args) { - await rokuDeploy.keyPress(args.text); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.keyPress(options); } } diff --git a/src/commands/KeyUpCommand.ts b/src/commands/KeyUpCommand.ts index 5e7e66d..36b71ea 100644 --- a/src/commands/KeyUpCommand.ts +++ b/src/commands/KeyUpCommand.ts @@ -1,7 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class KeyUpCommand { async run(args) { - await rokuDeploy.keyUp(args.text); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.keyUp(options); } } diff --git a/src/commands/PrepublishCommand.ts b/src/commands/PrepublishCommand.ts index 48152ed..e44613b 100644 --- a/src/commands/PrepublishCommand.ts +++ b/src/commands/PrepublishCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class PrepublishCommand { async run(args) { - await rokuDeploy.stage({ - stagingDir: args.stagingDir, - rootDir: args.rootDir - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.stage(options); } } diff --git a/src/commands/PublishCommand.ts b/src/commands/PublishCommand.ts index 7338471..e365b12 100644 --- a/src/commands/PublishCommand.ts +++ b/src/commands/PublishCommand.ts @@ -1,12 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class PublishCommand { async run(args) { - await rokuDeploy.sideload({ - host: args.host, - password: args.password, - outDir: args.outDir, - outFile: args.outFile - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.sideload(options); } } diff --git a/src/commands/RekeyDeviceCommand.ts b/src/commands/RekeyDeviceCommand.ts index 7098772..ca00fd8 100644 --- a/src/commands/RekeyDeviceCommand.ts +++ b/src/commands/RekeyDeviceCommand.ts @@ -1,14 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class RekeyDeviceCommand { async run(args) { - await rokuDeploy.rekeyDevice({ - host: args.host, - password: args.password, - rekeySignedPackage: args.rekeySignedPackage, - signingPassword: args.signingPassword, - rootDir: args.rootDir, - devId: args.devId - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.rekeyDevice(options); } } diff --git a/src/commands/SendTextCommand.ts b/src/commands/SendTextCommand.ts index a71db16..3e11810 100644 --- a/src/commands/SendTextCommand.ts +++ b/src/commands/SendTextCommand.ts @@ -1,8 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class SendTextCommand { - // this.options = getDefaultArgsFromJson(this.configPath ?? `${cwd}/rokudeploy.json`);TODO async run(args) { - await rokuDeploy.sendText(args.text); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.sendText(options); } } diff --git a/src/commands/TakeScreenshotCommand.ts b/src/commands/TakeScreenshotCommand.ts index 2d9f358..c77ef61 100644 --- a/src/commands/TakeScreenshotCommand.ts +++ b/src/commands/TakeScreenshotCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class TakeScreenshotCommand { async run(args) { - await rokuDeploy.captureScreenshot({ - host: args.host, - password: args.password - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.captureScreenshot(options); } } diff --git a/src/commands/ZipCommand.ts b/src/commands/ZipCommand.ts index 44b0e12..b03e80d 100644 --- a/src/commands/ZipCommand.ts +++ b/src/commands/ZipCommand.ts @@ -1,10 +1,11 @@ -import { rokuDeploy } from '../index'; +import { rokuDeploy, util } from '../index'; export class ZipCommand { async run(args) { - await rokuDeploy.zip({ - stagingDir: args.stagingDir, - outDir: args.outDir - }); + let options = { + ...util.getOptionsFromJson(args), + ...args + }; + await rokuDeploy.zip(options); } } diff --git a/src/util.spec.ts b/src/util.spec.ts index ea34990..b30032e 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -1,12 +1,10 @@ -import { util, standardizePath as s, standardizePathPosix as sp } from './util'; +import { util, standardizePath as s } from './util'; import { expect } from 'chai'; import * as fsExtra from 'fs-extra'; -import { cwd, tempDir, rootDir, outDir, expectThrowsAsync, writeFiles } from './testUtils.spec'; +import { cwd, tempDir, rootDir } from './testUtils.spec'; import * as path from 'path'; import * as dns from 'dns'; import { createSandbox } from 'sinon'; -import { RokuDeploy } from './RokuDeploy'; -import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; const sinon = createSandbox(); describe('util', () => { @@ -453,608 +451,30 @@ describe('util', () => { }); }); - describe('getFilePaths', () => { - const otherProjectName = 'otherProject'; - const otherProjectDir = sp`${rootDir}/../${otherProjectName}`; - let rokuDeploy: RokuDeploy; - let options: RokuDeployOptions; - //create baseline project structure + describe.only('getOptionsFromJson', () => { beforeEach(() => { - rokuDeploy = new RokuDeploy(); - options = rokuDeploy.getOptions({}); - fsExtra.ensureDirSync(`${rootDir}/components/emptyFolder`); - writeFiles(rootDir, [ - `manifest`, - `source/main.brs`, - `source/lib.brs`, - `components/component1.xml`, - `components/component1.brs`, - `components/screen1/screen1.xml`, - `components/screen1/screen1.brs` - ]); - }); - - async function getFilePaths(files: FileEntry[], rootDirOverride = rootDir) { - return (await util.getFilePaths(files, rootDirOverride)) - .sort((a, b) => a.src.localeCompare(b.src)); - } - - describe('top-level-patterns', () => { - it('excludes a file that is negated', async () => { - expect(await getFilePaths([ - 'source/**/*', - '!source/main.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('excludes file from non-rootdir top-level pattern', async () => { - writeFiles(rootDir, ['../externalDir/source/main.brs']); - expect(await getFilePaths([ - '../externalDir/**/*', - '!../externalDir/**/*' - ])).to.eql([]); - }); - - it('throws when using top-level string referencing file outside the root dir', async () => { - writeFiles(rootDir, [`../source/main.brs`]); - await expectThrowsAsync(async () => { - await getFilePaths([ - '../source/**/*' - ]); - }, 'Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); - }); - - it('works for brighterscript files', async () => { - writeFiles(rootDir, ['src/source/main.bs']); - expect(await getFilePaths([ - 'manifest', - 'source/**/*.bs' - ], s`${rootDir}/src`)).to.eql([{ - src: s`${rootDir}/src/source/main.bs`, - dest: s`source/main.bs` - }]); - }); - - it('works for root-level double star in top-level pattern', async () => { - expect(await getFilePaths([ - '**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for multile entries', async () => { - expect(await getFilePaths([ - 'source/**/*', - 'components/**/*', - 'manifest' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies top-level-string single-star globs', async () => { - writeFiles(rootDir, [ - 'source/lib.brs', - 'source/main.brs' - ]); - expect(await getFilePaths([ - 'source/*.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for double-star globs', async () => { - expect(await getFilePaths([ - '**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies subdir-level relative double-star globs', async () => { - expect(await getFilePaths([ - 'components/**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }]); - }); - - it('Finds folder using square brackets glob pattern', async () => { - fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); - expect(await getFilePaths([ - '[test]/*' - ], - rootDir - )).to.eql([{ - src: s`${rootDir}/e/file.brs`, - dest: s`e/file.brs` - }]); - }); - - it('Finds folder with escaped square brackets glob pattern as name', async () => { - fsExtra.outputFileSync(`${rootDir}/[test]/file.brs`, ''); - fsExtra.outputFileSync(`${rootDir}/e/file.brs`, ''); - expect(await getFilePaths([ - '\\[test\\]/*' - ], - rootDir - )).to.eql([{ - src: s`${rootDir}/[test]/file.brs`, - dest: s`[test]/file.brs` - }]); - }); - - it('throws exception when top-level strings reference files not under rootDir', async () => { - writeFiles(otherProjectDir, [ - 'manifest' - ]); - await expectThrowsAsync( - getFilePaths([ - `../${otherProjectName}/**/*` - ]) - ); - }); - - it('applies negated patterns', async () => { - expect(await getFilePaths([ - //include all components - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - - it('handles negated multi-globs', async () => { - expect((await getFilePaths([ - 'components/**/*', - '!components/screen1/**/*' - ])).map(x => x.dest)).to.eql([ - s`components/component1.brs`, - s`components/component1.xml` - ]); - }); - - it('allows negating paths outside rootDir without requiring src;dest; syntax', async () => { - fsExtra.outputFileSync(`${rootDir}/../externalLib/source/lib.brs`, ''); - const filePaths = await getFilePaths([ - 'source/**/*', - { src: '../externalLib/**/*', dest: 'source' }, - '!../externalLib/source/**/*' - ], rootDir); - expect( - filePaths.map(x => s`${x.src}`).sort() - ).to.eql([ - s`${rootDir}/source/lib.brs`, - s`${rootDir}/source/main.brs` - ]); - }); - - it('applies multi-glob paths relative to rootDir', async () => { - expect(await getFilePaths([ - 'manifest', - 'source/**/*', - 'components/**/*', - '!components/scenes/**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('ignores non-glob folder paths', async () => { - expect(await getFilePaths([ - //this is the folder called "components" - 'components' - ])).to.eql([]); //there should be no matches because rokudeploy ignores folders - }); - - }); - - describe('{src;dest} objects', () => { - it('excludes a file that is negated in src;dest;', async () => { - expect(await getFilePaths([ - 'source/**/*', - { - src: '!source/main.brs' - } - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('works for root-level double star in {src;dest} object', async () => { - expect(await getFilePaths([{ - src: '**/*', - dest: '' - } - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('uses the root of staging folder for dest when not specified with star star', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/**/*` - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`source/thirdPartyLib.brs` - }]); - }); - - it('copies absolute path files to specified dest', async () => { - writeFiles(otherProjectDir, [ - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/source/thirdPartyLib.brs`, - dest: 'lib/thirdPartyLib.brs' - }])).to.eql([{ - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`lib/thirdPartyLib.brs` - }]); - }); - - it('copies relative path files to specified dest', async () => { - const rootDirName = path.basename(rootDir); - writeFiles(rootDir, [ - 'source/main.brs' - ]); - expect(await getFilePaths([{ - src: `../${rootDirName}/source/main.brs`, - dest: 'source/main.brs' - }])).to.eql([{ - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('maintains relative path after **', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `../otherProject/**/*`, - dest: 'outFolder/' - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`outFolder/components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`outFolder/manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`outFolder/source/thirdPartyLib.brs` - }]); - }); - - it('works for other globs', async () => { - expect(await getFilePaths([{ - src: `components/screen1/*creen1.brs`, - dest: s`/source` - }])).to.eql([{ - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`source/screen1.brs` - }]); - }); - - it('applies negated patterns', async () => { - writeFiles(rootDir, [ - 'components/component1.brs', - 'components/component1.xml', - 'components/screen1/screen1.brs', - 'components/screen1/screen1.xml' - ]); - expect(await getFilePaths([ - //include all component brs files - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - }); - - it('converts relative rootDir path to absolute', async () => { - let stub = sinon.stub(rokuDeploy, 'getOptions').callThrough(); - await getFilePaths([ - 'source/main.brs' - ], './rootDir'); - expect(stub.callCount).to.be.greaterThan(0); - expect(stub.getCall(0).args[0].rootDir).to.eql('./rootDir'); - expect(stub.getCall(0).returnValue.rootDir).to.eql(s`${cwd}/rootDir`); - }); - - it('works when using a different current working directory than rootDir', async () => { - writeFiles(rootDir, [ - 'manifest', - 'images/splash_hd.jpg' - ]); - //sanity check, make sure it works without fiddling with cwd intact - let paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); - - //change the working directory and verify everything still works - - let wrongCwd = path.dirname(path.resolve(options.rootDir)); - process.chdir(wrongCwd); - - paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); + fsExtra.ensureDirSync(rootDir); + process.chdir(rootDir); }); - - it('supports absolute paths from outside of the rootDir', async () => { - //dest not specified - expect(await getFilePaths([{ - src: sp`${cwd}/README.md` - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`README.md` - }]); - - //dest specified - expect(await getFilePaths([{ - src: sp`${cwd}/README.md`, - dest: 'docs/README.md' - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - let paths: any[]; - - paths = await getFilePaths([{ - src: sp`${cwd}/README.md`, - dest: s`docs/README.md` - }], outDir); - - expect(paths).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - //top-level string paths pointing to files outside the root should thrown an exception - await expectThrowsAsync(async () => { - paths = await getFilePaths([ - sp`${cwd}/README.md` - ], outDir); + it('should fill in options from rokudeploy.json', () => { + fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); + expect( + util.getOptionsFromJson({ cwd: rootDir }) + ).to.eql({ + password: 'password' }); }); - it('supports relative paths that grab files from outside of the rootDir', async () => { - writeFiles(`${rootDir}/../`, [ - 'README.md' - ]); - expect( - await getFilePaths([{ - src: sp`../README.md` - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`README.md` - }]); - + it(`can't find file`, () => { + fsExtra.writeJsonSync(s`${rootDir}/rokudeployed.json`, { host: '1.2.3.4' }); expect( - await getFilePaths([{ - src: sp`../README.md`, - dest: 'docs/README.md' - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`docs/README.md` - }]); - }); - - it('should throw exception because we cannot have top-level string paths pointed to files outside the root', async () => { - writeFiles(rootDir, [ - '../README.md' - ]); - await expectThrowsAsync( - getFilePaths([ - path.posix.join('..', 'README.md') - ], outDir) - ); - }); + util.getOptionsFromJson({ cwd: rootDir }) + ).to.eql(undefined); - it('supports overriding paths', async () => { - let paths = await getFilePaths([{ - src: sp`${rootDir}/components/component1.brs`, - dest: 'comp1.brs' - }, { - src: sp`${rootDir}/components/screen1/screen1.brs`, - dest: 'comp1.brs' - }], rootDir); - expect(paths).to.be.lengthOf(1); - expect(s`${paths[0].src}`).to.equal(s`${rootDir}/components/screen1/screen1.brs`); }); - it('supports overriding paths from outside the root dir', async () => { - let thisRootDir = s`${tempDir}/tempTestOverrides/src`; + it(`loads cwd from process`, () => { try { - - fsExtra.ensureDirSync(s`${thisRootDir}/source`); - fsExtra.ensureDirSync(s`${thisRootDir}/components`); - fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); - - fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); - - let files = [ - '**/*.xml', - '**/*.brs', - { - src: '../.tmp/MainScene.brs', - dest: 'components/MainScene.brs' - } - ]; - let paths = await getFilePaths(files, thisRootDir); - - //the MainScene.brs file from source should NOT be included - let mainSceneEntries = paths.filter(x => s`${x.dest}` === s`components/MainScene.brs`); expect( mainSceneEntries, `Should only be one files entry for 'components/MainScene.brs'` @@ -1064,84 +484,6 @@ describe('util', () => { //clean up await fsExtra.remove(s`${thisRootDir}/../`); } - }); - - it('maintains original file path', async () => { - fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); - expect( - await getFilePaths([ - 'components/CustomButton.brs' - ], rootDir) - ).to.eql([{ - src: s`${rootDir}/components/CustomButton.brs`, - dest: s`components/CustomButton.brs` - }]); - }); - - it('correctly assumes file path if not given', async () => { - fsExtra.outputFileSync(`${rootDir}/components/CustomButton.brs`, ''); - expect( - (await getFilePaths([ - { src: 'components/*' } - ], rootDir)).sort((a, b) => a.src.localeCompare(b.src)) - ).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/CustomButton.brs`, - dest: s`components/CustomButton.brs` - }]); - }); - }); - - describe('getOptionsFromJson', () => { - beforeEach(() => { - fsExtra.ensureDirSync(rootDir); - process.chdir(rootDir); - }); - it('should fill in missing options from rokudeploy.json', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); - let options = util.getOptionsFromJson({ - rootDir: `${rootDir}`, - host: '1.2.3.4' - }); - let expectedOutput = { - rootDir: `${rootDir}`, - host: '1.2.3.4', - password: 'password' - }; - expect(options).to.eql(expectedOutput); - }); - - it('should fill in missing default options from bsconfig.json', () => { - fsExtra.writeJsonSync(s`${rootDir}/bsconfig.json`, { password: 'password' }); - let options = util.getOptionsFromJson({ - rootDir: `${rootDir}`, - host: '1.2.3.4' - }); - let expectedOutput = { - rootDir: `${rootDir}`, - host: '1.2.3.4', - password: 'password' - }; - expect(options).to.eql(expectedOutput); - - }); - - it('should not replace default options', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { host: '4.3.2.1' }); - let options = util.getOptionsFromJson({ - rootDir: `${rootDir}`, - host: '1.2.3.4' - }); - let expectedOutput = { - rootDir: `${rootDir}`, - host: '1.2.3.4', - }; - expect(options).to.eql(expectedOutput); }); }); From 053a3f4471179e678354efa8ef1b11b97387bf6b Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 5 Mar 2024 12:57:34 -0500 Subject: [PATCH 43/63] remove bsconfig, fix cli file with key press commands, fix key press functions and add options --- src/RokuDeploy.spec.ts | 17 ------------ src/RokuDeploy.ts | 62 ++++++++++++++++++++++++++++++++---------- src/cli.ts | 62 +++++++++++++++++++++++------------------- 3 files changed, 81 insertions(+), 60 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 02ef5de..9f51329 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -3215,11 +3215,6 @@ describe('index', () => { expect(rokuDeploy.getOptions().outFile).to.equal('rokudeploy-outfile'); }); - it('if bsconfig.json config file is available it should use those values instead of the default', () => { - fsExtra.writeJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('bsconfig-outfile'); - }); - it('if rokudeploy.json config file is available and bsconfig.json is also available it should use rokudeploy.json instead of bsconfig.json', () => { fsExtra.outputJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); fsExtra.outputJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); @@ -3227,23 +3222,11 @@ describe('index', () => { }); it('if runtime options are provided, they should override any existing config file options', () => { - fsExtra.writeJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); fsExtra.writeJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); expect(rokuDeploy.getOptions({ outFile: 'runtime-outfile' }).outFile).to.equal('runtime-outfile'); }); - - it('if runtime config should override any existing config file options', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - fsExtra.writeJsonSync(s`${rootDir}/bsconfig`, { outFile: 'rokudeploy-outfile' }); - - fsExtra.writeJsonSync(s`${rootDir}/brsconfig.json`, { outFile: 'project-config-outfile' }); - options = { - project: 'brsconfig.json' - }; - expect(rokuDeploy.getOptions(options).outFile).to.equal('project-config-outfile'); - }); }); }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index a6c81f7..91d02a9 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -183,33 +183,35 @@ export class RokuDeploy { return baseRequestOptions; } - public async keypress(options: { key: string }) { + public async keyPress(options: KeyPressOptions) { return this.sendKeyEvent({ ...options, + key: 'all.the.others', action: 'keypress' }); } - public async keyup(options: any) { + public async keyUp(options: KeyUpOptions) { return this.sendKeyEvent({ ...options, action: 'keyup' }); } - public async keydown(options: any) { + public async keyDown(options: KeyDownOptions) { return this.sendKeyEvent({ ...options, action: 'keydown' }); } - public async sendText(options: any) { + public async sendText(options: SendTextOptions) { const chars = options.text.split(''); for (const char of chars) { - await this.keypress({ + await this.sendKeyEvent({ ...options, - key: `lit_${char}` + key: `lit_${char}`, + action: 'keypress' }); } } @@ -217,18 +219,13 @@ export class RokuDeploy { /** * Simulate pressing the home button on the remote for this roku. * This makes the roku return to the home screen - * @param host - the host - * @param port - the port that should be used for the request. defaults to 8060 - * @param timeout - request timeout duration in milliseconds. defaults to 150000 */ - private async sendKeyEvent(options: { host: string; port?: string; key: 'home' | 'left' | 'all.the.others'; action: 'keypress' | 'keyup' | 'keydown'; timeout?: number }) { - let options = this.getOptions(); - port = port ? port : options.remotePort; - timeout = timeout ? timeout : options.timeout; + private async sendKeyEvent(options: SendKeyEventOptions) { + let filledOptions = this.getOptions(options); // press the home button to return to the main screen return this.doPostRequest({ - url: `http://${host}:${port}/keypress/Home`, - timeout: timeout + url: `http://${filledOptions.host}:${filledOptions.remotePort}/${filledOptions.action}/${filledOptions.key}`, + timeout: filledOptions.timeout }, false); } @@ -964,6 +961,41 @@ export interface GetDeviceInfoOptions { enhance?: boolean; } +type RokuKey = 'home' | 'rev' | 'fwd' | 'play' | 'select' | 'left' | 'right' | 'down' | 'up' | 'back' | 'instantreplay' | 'info' | 'backspace' | 'search' | 'enter' | 'findremote' | 'volumeup' | 'volumedown' | 'volumemute' | 'poweroff' | 'channelup' | 'channeldown' | 'inputtuner' | 'inputhdmi1' | 'inputhdmi2' | 'inputhdmi3' | 'inputhdmi4' | 'inputav1'; +export interface SendKeyEventOptions { + action?: 'keypress' | 'keydown' | 'keyup'; + host: string; + key: RokuKey | string; + remotePort?: number; + timeout?: number; +} + +export interface KeyUpOptions extends SendKeyEventOptions { + action?: 'keyup'; + key: RokuKey; +} + +export interface KeyDownOptions extends SendKeyEventOptions { + action?: 'keydown'; + key: RokuKey; +} + +export interface KeyPressOptions extends SendKeyEventOptions { + action?: 'keypress'; + key: RokuKey; +} + +export interface SendTextOptions extends SendKeyEventOptions { + action?: 'keypress'; + text: string; +} + +export interface CloseChannelOptions { + host: string; + remotePort: number; + timeout?: number; + +} export interface StageOptions { rootDir?: string; files?: FileEntry[]; diff --git a/src/cli.ts b/src/cli.ts index 8f4c249..f3c3ead 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,7 @@ #!/usr/bin/env node import * as yargs from 'yargs'; -import { rokuDeploy } from './index'; import { ExecCommand } from './commands/ExecCommand'; -import { TextCommand } from './commands/TextCommand'; +import { SendTextCommand } from './commands/SendTextCommand'; import { PrepublishCommand } from './commands/PrepublishCommand'; import { ZipPackageCommand } from './commands/ZipPackageCommand'; import { PublishCommand } from './commands/PublishCommand'; @@ -16,6 +15,10 @@ import { GetOutputPkgFilePathCommand } from './commands/GetOutputPkgFilePathComm import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; import { ZipCommand } from './commands/ZipCommand'; +import { KeyPressCommand } from './commands/KeyPressCommand'; +import { KeyUpCommand } from './commands/KeyUpCommand'; +import { KeyDownCommand } from './commands/KeyDownCommand'; +import type { RokuDeploy } from './RokuDeploy'; void yargs @@ -57,32 +60,44 @@ void yargs return new ExecCommand(args.actions, args.configPath).run(); }) - .command(['sendText', 'text'], 'Send text command', (builder) => { + .command('keypress', 'send keypress command', (builder) => { return builder - .option('text', { type: 'string', description: 'The text to send', demandOption: true }); + .option('key', { type: 'string', description: 'The key to send', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }); }, (args: any) => { - return new TextCommand().run(args); //TODO: do this through exec command to get default args like host and port? or add those to here and go through separate command for better testing + return new KeyPressCommand().run(args); }) - .command('keypress', 'send keypress command', (builder) => { + .command('keyup', 'send keyup command', (builder) => { return builder - .option('key', { type: 'string', description: 'The key to send', demandOption: true }); - }, async (args: any) => { - await rokuDeploy.keyPress(args.text); //TODO: Go through exec command, separate command, or use key event? + .option('key', { type: 'string', description: 'The key to send', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }); + }, (args: any) => { + return new KeyUpCommand().run(args); }) - .command('keyup', 'send keyup command', (builder) => { + .command('keydown', 'send keydown command', (builder) => { return builder - .option('key', { type: 'string', description: 'The key to send', demandOption: true }); - }, async (args: any) => { - await rokuDeploy.keyUp(args.text); //TODO: Go through exec command, separate command, or use key event? + .option('key', { type: 'string', description: 'The key to send', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }); + }, (args: any) => { + return new KeyDownCommand().run(args); }) - .command('keydown', 'send keydown command', (builder) => { + .command(['sendText', 'text'], 'Send text command', (builder) => { return builder - .option('key', { type: 'string', description: 'The key to send', demandOption: true }); - }, async (args: any) => { - await rokuDeploy.keyDown(args.text); //TODO: Go through exec command, separate command, or use key event? + .option('text', { type: 'string', description: 'The text to send', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }); + }, (args: any) => { + return new SendTextCommand().run(args); //TODO: Add default options }) .command(['stage', 'prepublishToStaging'], 'Copies all of the referenced files to the staging folder', (builder) => { @@ -141,21 +156,12 @@ void yargs return new CreateSignedPackageCommand().run(args); }) - .command('deploy', 'Deploy a pre-existing packaged zip file to a remote Roku', (builder) => { - return builder - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) - .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) - .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }); - }, (args: any) => { - return new DeployCommand().run(args); - }) - .command(['deleteDevChannel', 'deleteInstalledChannel', 'rmdev', 'delete'], 'Delete an installed channel', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); }, (args: any) => { - return new DeleteInstalledChannelCommand().run(args); + return new DeleteDevChannelCommand().run(args); }) .command(['screenshot', 'captureScreenshot'], 'Take a screenshot', (builder) => { @@ -163,7 +169,7 @@ void yargs .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); }, (args: any) => { - return new TakeScreenshotCommand().run(args);//TODO: rename + return new TakeScreenshotCommand().run(args); }) .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { From fa077870bd1ae8fa7b2e8b263306d6d8259de7d9 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 5 Mar 2024 12:58:12 -0500 Subject: [PATCH 44/63] fix getoptionsfromjson --- src/util.spec.ts | 22 +++++++--------------- src/util.ts | 36 +++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/util.spec.ts b/src/util.spec.ts index b30032e..74c490e 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -451,7 +451,7 @@ describe('util', () => { }); }); - describe.only('getOptionsFromJson', () => { + describe('getOptionsFromJson', () => { beforeEach(() => { fsExtra.ensureDirSync(rootDir); process.chdir(rootDir); @@ -465,24 +465,16 @@ describe('util', () => { }); }); - it(`can't find file`, () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeployed.json`, { host: '1.2.3.4' }); - expect( - util.getOptionsFromJson({ cwd: rootDir }) - ).to.eql(undefined); - - }); - it(`loads cwd from process`, () => { try { + fsExtra.writeJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); expect( - mainSceneEntries, - `Should only be one files entry for 'components/MainScene.brs'` - ).to.be.lengthOf(1); - expect(s`${mainSceneEntries[0].src}`).to.eql(s`${thisRootDir}/../.tmp/MainScene.brs`); + util.getOptionsFromJson() + ).to.eql({ + host: '1.2.3.4' + }); } finally { - //clean up - await fsExtra.remove(s`${thisRootDir}/../`); + fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); } }); diff --git a/src/util.ts b/src/util.ts index 2895b06..56df6d3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -449,23 +449,25 @@ export class Util { const cwd = options?.cwd ?? process.cwd(); const configPath = path.join(cwd, 'rokudeploy.json'); - 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 - }; - }) - )); + 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; } From 43446661cf3ac18fd6bb1b96f552a1395fbf2f9e Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 5 Mar 2024 23:36:44 -0500 Subject: [PATCH 45/63] delete unnecessary commands, fix some tests --- src/RokuDeploy.spec.ts | 40 ++++++++++----------- src/RokuDeploy.ts | 8 ++--- src/RokuDeployOptions.ts | 5 +++ src/cli.ts | 27 +++----------- src/commands/GetOutputPkgFilePathCommand.ts | 13 ------- src/commands/GetOutputZipFilePathCommand.ts | 13 ------- 6 files changed, 33 insertions(+), 73 deletions(-) delete mode 100644 src/commands/GetOutputPkgFilePathCommand.ts delete mode 100644 src/commands/GetOutputZipFilePathCommand.ts diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 9f51329..59eec4f 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1877,35 +1877,35 @@ describe('index', () => { describe('normalizeFilesArray', () => { it('catches invalid dest entries', () => { expect(() => { - rokuDeploy['normalizeFilesArray']([{ + util['normalizeFilesArray']([{ src: 'some/path', dest: true }]); }).to.throw(); expect(() => { - rokuDeploy['normalizeFilesArray']([{ + util['normalizeFilesArray']([{ src: 'some/path', dest: false }]); }).to.throw(); expect(() => { - rokuDeploy['normalizeFilesArray']([{ + util['normalizeFilesArray']([{ src: 'some/path', dest: /asdf/gi }]); }).to.throw(); expect(() => { - rokuDeploy['normalizeFilesArray']([{ + util['normalizeFilesArray']([{ src: 'some/path', dest: {} }]); }).to.throw(); expect(() => { - rokuDeploy['normalizeFilesArray']([{ + util['normalizeFilesArray']([{ src: 'some/path', dest: [] }]); @@ -1913,7 +1913,7 @@ describe('index', () => { }); it('normalizes directory separators paths', () => { - expect(rokuDeploy['normalizeFilesArray']([{ + expect(util['normalizeFilesArray']([{ src: `long/source/path`, dest: `long/dest/path` }])).to.eql([{ @@ -1923,7 +1923,7 @@ describe('index', () => { }); it('works for simple strings', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ 'manifest', 'source/main.brs' ])).to.eql([ @@ -1933,7 +1933,7 @@ describe('index', () => { }); it('works for negated strings', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ '!.git' ])).to.eql([ '!.git' @@ -1941,7 +1941,7 @@ describe('index', () => { }); it('skips falsey and bogus entries', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ '', 'manifest', false, @@ -1953,7 +1953,7 @@ describe('index', () => { }); it('works for {src:string} objects', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ { src: 'manifest' } @@ -1964,7 +1964,7 @@ describe('index', () => { }); it('works for {src:string[]} objects', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ { src: [ 'manifest', @@ -1981,7 +1981,7 @@ describe('index', () => { }); it('retains dest option', () => { - expect(rokuDeploy['normalizeFilesArray']([ + expect(util['normalizeFilesArray']([ { src: 'source/config.dev.brs', dest: 'source/config.brs' @@ -1993,14 +1993,14 @@ describe('index', () => { }); it('throws when encountering invalid entries', () => { - expect(() => rokuDeploy['normalizeFilesArray']([true])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([/asdf/])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([new Date()])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([1])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: true }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: /asdf/ }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: new Date() }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: 1 }])).to.throw(); + expect(() => util['normalizeFilesArray']([true])).to.throw(); + expect(() => util['normalizeFilesArray']([/asdf/])).to.throw(); + expect(() => util['normalizeFilesArray']([new Date()])).to.throw(); + expect(() => util['normalizeFilesArray']([1])).to.throw(); + expect(() => util['normalizeFilesArray']([{ src: true }])).to.throw(); + expect(() => util['normalizeFilesArray']([{ src: /asdf/ }])).to.throw(); + expect(() => util['normalizeFilesArray']([{ src: new Date() }])).to.throw(); + expect(() => util['normalizeFilesArray']([{ src: 1 }])).to.throw(); }); }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 91d02a9..29055ab 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -69,7 +69,7 @@ export class RokuDeploy { * Given an already-populated staging folder, create a zip archive of it and copy it to the output folder * @param options */ - public async zip(options: ZipPackageOptions) { + public async zip(options: ZipOptions) { options = this.getOptions(options) as any; //make sure the output folder exists @@ -708,8 +708,8 @@ export class RokuDeploy { this.logger.logLevel = finalOptions.logLevel; //TODO: Handle logging differently //fully resolve the folder paths - finalOptions.rootDir = path.resolve(options.cwd, finalOptions.rootDir); - finalOptions.outDir = path.resolve(options.cwd, finalOptions.outDir); + finalOptions.rootDir = path.resolve(finalOptions.cwd, finalOptions.rootDir); + finalOptions.outDir = path.resolve(finalOptions.cwd, finalOptions.outDir); //stagingDir if (options.stagingDir) { @@ -1003,7 +1003,7 @@ export interface StageOptions { retainStagingDir?: boolean; } -export interface ZipPackageOptions { +export interface ZipOptions { stagingDir?: string; outDir?: string; } diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index dc1b4ff..a70b766 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -1,6 +1,11 @@ import type { LogLevel } from './Logger'; export interface RokuDeployOptions { + /** + * The working directory where the command should be executed + */ + cwd?: string; + /** * Path to a bsconfig.json project file */ diff --git a/src/cli.ts b/src/cli.ts index f3c3ead..360ec35 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,8 +10,6 @@ import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; -import { GetOutputZipFilePathCommand } from './commands/GetOutputZipFilePathCommand'; -import { GetOutputPkgFilePathCommand } from './commands/GetOutputPkgFilePathCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; import { ZipCommand } from './commands/ZipCommand'; @@ -172,23 +170,6 @@ void yargs return new TakeScreenshotCommand().run(args); }) - .command('getOutputZipFilePath', 'Centralizes getting output zip file path based on passed in options', (builder) => { - return builder - .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); - return builder; - }, (args: any) => { - return new GetOutputZipFilePathCommand().run(args); - }) - - .command('getOutputPkgFilePath', 'Centralizes getting output pkg file path based on passed in options', (builder) => { - return builder - .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); - }, (args: any) => { - return new GetOutputPkgFilePathCommand().run(args); - }) - .command(['getDeviceInfo', 'deviceinfo'], 'Get the `device-info` response from a Roku device', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }); @@ -203,13 +184,13 @@ void yargs return new GetDevIdCommand().run(args); }) - .command('zipFolder', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { + .command('zip', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { return builder - .option('srcFolder', { type: 'string', description: 'The folder that should be zipped', demandOption: false }) - .option('zipFilePath', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: false }); + .option('stagingDir', { type: 'string', description: 'The folder that should be zipped', demandOption: false }) + .option('outDir', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: false }); }, (args: any) => { console.log('args', args); - return new ZipFolderCommand().run(args); + return new ZipCommand().run(args); }) .argv; diff --git a/src/commands/GetOutputPkgFilePathCommand.ts b/src/commands/GetOutputPkgFilePathCommand.ts deleted file mode 100644 index 3bfc2d0..0000000 --- a/src/commands/GetOutputPkgFilePathCommand.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { rokuDeploy, util } from '../index'; - -export class GetOutputPkgFilePathCommand { - run(args) { - let options = { - ...util.getOptionsFromJson(args), - ...args - }; - // eslint-disable-next-line @typescript-eslint/dot-notation - const outputPath = rokuDeploy['getOutputPkgPath'](options); //TODO fix this? - console.log(outputPath); - } -} diff --git a/src/commands/GetOutputZipFilePathCommand.ts b/src/commands/GetOutputZipFilePathCommand.ts deleted file mode 100644 index c5761c0..0000000 --- a/src/commands/GetOutputZipFilePathCommand.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { rokuDeploy, util } from '../index'; - -export class GetOutputZipFilePathCommand { - run(args) { - let options = { - ...util.getOptionsFromJson(args), - ...args - }; - // eslint-disable-next-line @typescript-eslint/dot-notation - const outputPath = rokuDeploy['getOutputZipFilePath'](options); - console.log(outputPath); - } -} From 7d9e56e9f75cbf90e4f5b1b4317561343504e435 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 5 Mar 2024 23:42:56 -0500 Subject: [PATCH 46/63] change to captureScreenshot --- src/RokuDeploy.spec.ts | 2 +- src/RokuDeploy.ts | 4 ++-- src/cli.spec.ts | 4 ++-- src/cli.ts | 4 ++-- .../{TakeScreenshotCommand.ts => CaptureScreenshotCommand.ts} | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/commands/{TakeScreenshotCommand.ts => CaptureScreenshotCommand.ts} (85%) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 59eec4f..eda8994 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -2016,7 +2016,7 @@ describe('index', () => { }); }); - describe('takeScreenshot', () => { + describe('captureScreenshot', () => { let onHandler: any; let screenshotAddress: any; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 29055ab..777aedc 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -614,7 +614,7 @@ export class RokuDeploy { /** * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ - public async captureScreenshot(options: TakeScreenshotOptions) { + public async captureScreenshot(options: CaptureScreenshotOptions) { options = this.getOptions(options); options.screenshotFile = options.screenshotFile ?? `screenshot-${dayjs().format('YYYY-MM-DD-HH.mm.ss.SSS')}`; let saveFilePath: string; @@ -916,7 +916,7 @@ export interface HttpResponse { body: any; } -export interface TakeScreenshotOptions { +export interface CaptureScreenshotOptions { /** * The IP address or hostname of the target Roku device. * @example '192.168.1.21' diff --git a/src/cli.spec.ts b/src/cli.spec.ts index f599044..06ce457 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -10,7 +10,7 @@ import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; -import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; +import { CaptureScreenshotCommand } from './commands/CaptureScreenshotCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; @@ -185,7 +185,7 @@ describe('cli', () => { return Promise.resolve(''); }); - const command = new TakeScreenshotCommand(); + const command = new CaptureScreenshotCommand(); await command.run({ host: '1.2.3.4', password: '5536' diff --git a/src/cli.ts b/src/cli.ts index 360ec35..d2e5a9b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; -import { TakeScreenshotCommand } from './commands/TakeScreenshotCommand'; +import { CaptureScreenshotCommand } from './commands/CaptureScreenshotCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; import { ZipCommand } from './commands/ZipCommand'; @@ -167,7 +167,7 @@ void yargs .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }); }, (args: any) => { - return new TakeScreenshotCommand().run(args); + return new CaptureScreenshotCommand().run(args); }) .command(['getDeviceInfo', 'deviceinfo'], 'Get the `device-info` response from a Roku device', (builder) => { diff --git a/src/commands/TakeScreenshotCommand.ts b/src/commands/CaptureScreenshotCommand.ts similarity index 85% rename from src/commands/TakeScreenshotCommand.ts rename to src/commands/CaptureScreenshotCommand.ts index c77ef61..6a75ed5 100644 --- a/src/commands/TakeScreenshotCommand.ts +++ b/src/commands/CaptureScreenshotCommand.ts @@ -1,6 +1,6 @@ import { rokuDeploy, util } from '../index'; -export class TakeScreenshotCommand { +export class CaptureScreenshotCommand { async run(args) { let options = { ...util.getOptionsFromJson(args), From ebb3d22d0ca682891893fbc20a16d34edb94e1e9 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Fri, 8 Mar 2024 11:02:22 -0500 Subject: [PATCH 47/63] Fix a bunch of tests --- src/RokuDeploy.spec.ts | 230 ++++++--------------------------------- src/RokuDeploy.ts | 37 +++---- src/RokuDeployOptions.ts | 2 +- src/cli.spec.ts | 18 +-- src/util.spec.ts | 56 +++++++++- src/util.ts | 3 +- 6 files changed, 110 insertions(+), 236 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index eda8994..486f2d6 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -606,68 +606,6 @@ describe('index', () => { }); }); - describe('copyToStaging', () => { - it('throws exceptions when rootDir does not exist', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], 'staging', 'folder_does_not_exist') - ); - }); - - it('throws exceptions on missing stagingPath', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], undefined, undefined) - ); - }); - - it('throws exceptions on missing rootDir', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], 'asdf', undefined) - ); - }); - - it('computes absolute path for all operations', async () => { - const ensureDirPaths = []; - sinon.stub(fsExtra, 'ensureDir').callsFake((p) => { - ensureDirPaths.push(p); - return Promise.resolve; - }); - const copyPaths = [] as Array<{ src: string; dest: string }>; - sinon.stub(fsExtra as any, 'copy').callsFake((src, dest) => { - copyPaths.push({ src: src as string, dest: dest as string }); - return Promise.resolve(); - }); - - sinon.stub(rokuDeploy, 'getFilePaths').returns( - Promise.resolve([ - { - src: s`${rootDir}/source/main.brs`, - dest: '/source/main.brs' - }, { - src: s`${rootDir}/components/a/b/c/comp1.xml`, - dest: '/components/a/b/c/comp1.xml' - } - ]) - ); - - await rokuDeploy['copyToStaging']([], stagingDir, rootDir); - - expect(ensureDirPaths).to.eql([ - s`${stagingDir}/source`, - s`${stagingDir}/components/a/b/c` - ]); - - expect(copyPaths).to.eql([ - { - src: s`${rootDir}/source/main.brs`, - dest: s`${stagingDir}/source/main.brs` - }, { - src: s`${rootDir}/components/a/b/c/comp1.xml`, - dest: s`${stagingDir}/components/a/b/c/comp1.xml` - } - ]); - }); - }); - describe('zipPackage', () => { it('should throw error when manifest is missing', async () => { let err; @@ -733,7 +671,7 @@ describe('index', () => { it('uses default port', async () => { const promise = new Promise((resolve) => { sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/Home'); + expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/home'); resolve(); }); }); @@ -744,7 +682,7 @@ describe('index', () => { it('uses overridden port', async () => { const promise = new Promise((resolve) => { sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:987/keypress/Home'); + expect(opts.url).to.equal('http://1.2.3.4:987/keypress/home'); resolve(); }); }); @@ -755,7 +693,7 @@ describe('index', () => { it('uses default timeout', async () => { const promise = new Promise((resolve) => { sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/Home'); + expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/home'); expect(opts.timeout).to.equal(150000); resolve(); }); @@ -768,7 +706,7 @@ describe('index', () => { const promise = new Promise((resolve) => { sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:987/keypress/Home'); + expect(opts.url).to.equal('http://1.2.3.4:987/keypress/home'); expect(opts.timeout).to.equal(1000); resolve(); }); @@ -779,7 +717,7 @@ describe('index', () => { }); let fileCounter = 1; - describe('publish', () => { + describe('sideload', () => { beforeEach(() => { //make a dummy output file...we don't care what's in it options.outFile = `temp${fileCounter++}.zip`; @@ -860,7 +798,8 @@ describe('index', () => { host: '1.2.3.4', password: 'password', outDir: outDir, - outFile: 'fileThatDoesNotExist.zip' + outFile: 'fileThatDoesNotExist.zip', + deleteDevChannel: false }); }, `Cannot publish because file does not exist at '${rokuDeploy['getOutputZipFilePath']({ outFile: 'fileThatDoesNotExist.zip', @@ -1007,7 +946,8 @@ describe('index', () => { outDir: outDir, failOnCompileError: true, remoteDebug: true, - outFile: options.outFile + outFile: options.outFile, + deleteDevChannel: false }).then((result) => { expect(result.message).to.equal('Successful deploy'); expect(stub.getCall(0).args[0].formData.remotedebug).to.eql('1'); @@ -1026,7 +966,8 @@ describe('index', () => { failOnCompileError: true, remoteDebug: true, remoteDebugConnectEarly: true, - outFile: options.outFile + outFile: options.outFile, + deleteDevChannel: false }).then((result) => { expect(result.message).to.equal('Successful deploy'); expect(stub.getCall(0).args[0].formData.remotedebug_connect_early).to.eql('1'); @@ -1411,13 +1352,17 @@ describe('index', () => { node.appendChild(pkgDiv);`; mockDoPostRequest(body); + const stub = sinon.stub(rokuDeploy as any, 'downloadFile').returns(Promise.resolve('pkgs//P6953175d5df120c0069c53de12515b9a.pkg')); + let pkgPath = await rokuDeploy.createSignedPackage({ host: '1.2.3.4', password: 'password', signingPassword: options.signingPassword, - stagingDir: stagingDir + stagingDir: stagingDir, + outDir: outDir }); - expect(pkgPath).to.equal('pkgs//P6953175d5df120c0069c53de12515b9a.pkg'); + expect(pkgPath).to.equal(s`${outDir}/roku-deploy.pkg`); + expect(stub.getCall(0).args[0].url).to.equal('http://1.2.3.4:80/pkgs//P6953175d5df120c0069c53de12515b9a.pkg'); }); it('should return our fallback error if neither error or package link was detected', async () => { @@ -2161,7 +2106,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: `${tempDir}/myScreenShots` }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: `${tempDir}/myScreenShots` }); expect(result).not.to.be.undefined; expect(util.standardizePath(`${tempDir}/myScreenShots`)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(result)); @@ -2185,7 +2130,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: tempDir, screenshotFile: 'my' }); expect(result).not.to.be.undefined; expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(path.join(tempDir, 'my.png'))); @@ -2209,7 +2154,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my.jpg' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: tempDir, screenshotFile: 'my.jpg' }); expect(result).not.to.be.undefined; expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(path.join(tempDir, 'my.jpg.png'))); @@ -2256,7 +2201,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, outFile: 'myFile' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotFile: 'myFile' }); expect(result).not.to.be.undefined; expect(path.basename(result)).to.equal('myFile.jpg'); expect(fsExtra.existsSync(result)); @@ -3026,37 +2971,6 @@ describe('index', () => { }); }); - describe('stringifyManifest', () => { - it('correctly converts back to a valid manifest when lineNumber and keyIndexes are provided', () => { - expect( - rokuDeploy['stringifyManifest']( - rokuDeploy['parseManifestFromString']('major_version=3\nminor_version=4') - ) - ).to.equal( - 'major_version=3\nminor_version=4' - ); - }); - - it('correctly converts back to a valid manifest when lineNumber and keyIndexes are not provided', () => { - const parsed = rokuDeploy['parseManifestFromString']('title=App\nmajor_version=3'); - delete parsed.keyIndexes; - delete parsed.lineCount; - let outputParsedManifest = rokuDeploy['parseManifestFromString']( - rokuDeploy['stringifyManifest'](parsed) - ); - expect(outputParsedManifest.title).to.equal('App'); - expect(outputParsedManifest.major_version).to.equal('3'); - }); - }); - - describe('computeFileDestPath', () => { - it('treats {src;dest} without dest as a top-level string', () => { - expect( - rokuDeploy['computeFileDestPath'](s`${rootDir}/source/main.brs`, { src: s`source/main.brs` } as any, rootDir) - ).to.eql(s`source/main.brs`); - }); - }); - describe('checkRequest', () => { it('throws FailedDeviceResponseError when necessary', () => { sinon.stub(rokuDeploy as any, 'getRokuMessagesFromResponseBody').returns({ @@ -3093,89 +3007,23 @@ describe('index', () => { }); it('works when passing in stagingDir', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); options = rokuDeploy.getOptions({ stagingDir: './staging-dir' }); expect(options.stagingDir.endsWith('staging-dir')).to.be.true; }); - it('works when loading stagingDir from rokudeploy.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return true; - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "stagingDir": "./staging-dir" - } - `); - options = rokuDeploy.getOptions(); - expect(options.stagingDir.endsWith('staging-dir')).to.be.true; - }); - - it('supports jsonc for roku-deploy.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('rokudeploy.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - //leading comment - { - //inner comment - "rootDir": "src" //trailing comment - } - //trailing comment - `); - options = rokuDeploy.getOptions(undefined); - expect(options.rootDir).to.equal(path.join(process.cwd(), 'src')); - }); - - it('supports jsonc for bsconfig.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('bsconfig.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - //leading comment - { - //inner comment - "rootDir": "src" //trailing comment - } - //trailing comment - `); - options = rokuDeploy.getOptions(undefined); - expect(options.rootDir).to.equal(path.join(process.cwd(), 'src')); - }); - - it('catches invalid json with jsonc parser', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('bsconfig.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "rootDir": "src" - `); - let ex; - try { - rokuDeploy.getOptions(undefined); - } catch (e) { - ex = e; - } - expect(ex).to.exist; - expect(ex.message.startsWith('Error parsing')).to.be.true; - }); - it('does not error when no parameter provided', () => { expect(rokuDeploy.getOptions(undefined)).to.exist; }); - describe('deleteInstalledChannel', () => { + describe('deleteDevChannel', () => { it('defaults to true', () => { - expect(rokuDeploy.getOptions({}).deleteInstalledChannel).to.equal(true); + expect(rokuDeploy.getOptions({}).deleteDevChannel).to.equal(true); }); it('can be overridden', () => { - expect(rokuDeploy.getOptions({ deleteInstalledChannel: false }).deleteInstalledChannel).to.equal(false); + expect(rokuDeploy.getOptions({ deleteDevChannel: false }).deleteDevChannel).to.equal(false); }); }); @@ -3201,7 +3049,7 @@ describe('index', () => { }); }); - describe('config file', () => { + describe('default options', () => { beforeEach(() => { process.chdir(rootDir); }); @@ -3210,19 +3058,7 @@ describe('index', () => { expect(rokuDeploy.getOptions().outFile).to.equal('roku-deploy'); }); - it('if rokudeploy.json config file is available it should use those values instead of the default', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('rokudeploy-outfile'); - }); - - it('if rokudeploy.json config file is available and bsconfig.json is also available it should use rokudeploy.json instead of bsconfig.json', () => { - fsExtra.outputJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); - fsExtra.outputJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('rokudeploy-outfile'); - }); - - it('if runtime options are provided, they should override any existing config file options', () => { - fsExtra.writeJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); + it('if runtime options are provided, they should override any default options', () => { expect(rokuDeploy.getOptions({ outFile: 'runtime-outfile' }).outFile).to.equal('runtime-outfile'); @@ -3230,9 +3066,9 @@ describe('index', () => { }); }); - describe('getToFile', () => { + describe('downloadFile', () => { it('waits for the write stream to finish writing before resolving', async () => { - let getToFileIsResolved = false; + let downloadFileIsResolved = false; let requestCalled = q.defer(); let onResponse = q.defer<(res) => any>(); @@ -3254,25 +3090,25 @@ describe('index', () => { return req; }); - const finalPromise = rokuDeploy['getToFile']({}, s`${tempDir}/out/something.txt`).then(() => { - getToFileIsResolved = true; + const finalPromise = rokuDeploy['downloadFile']({}, s`${tempDir}/out/something.txt`).then(() => { + downloadFileIsResolved = true; }); await requestCalled.promise; - expect(getToFileIsResolved).to.be.false; + expect(downloadFileIsResolved).to.be.false; const callback = await onResponse.promise; callback({ statusCode: 200 }); await util.sleep(10); - expect(getToFileIsResolved).to.be.false; + expect(downloadFileIsResolved).to.be.false; const writeStream = await writeStreamPromise; writeStream.write('test'); writeStream.close(); await finalPromise; - expect(getToFileIsResolved).to.be.true; + expect(downloadFileIsResolved).to.be.true; }); }); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 777aedc..bc66be4 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -36,7 +36,6 @@ export class RokuDeploy { //make sure the staging folder exists await fsExtra.ensureDir(options.stagingDir); - // await this.copyToStaging(options.files, options.stagingDir, options.rootDir); if (!options.stagingDir) { throw new Error('stagingPath is required'); @@ -186,7 +185,7 @@ export class RokuDeploy { public async keyPress(options: KeyPressOptions) { return this.sendKeyEvent({ ...options, - key: 'all.the.others', + key: options.key, action: 'keypress' }); } @@ -445,11 +444,11 @@ export class RokuDeploy { let pkgSearchMatches = //.exec(results.body); if (pkgSearchMatches) { const url = pkgSearchMatches[1]; - options = this.getOptions(options) as any; let requestOptions2 = this.generateBaseRequestOptions(url, options); let pkgFilePath = this.getOutputPkgFilePath(options as any); - return this.getToFile(requestOptions2, pkgFilePath); + await this.downloadFile(requestOptions2, pkgFilePath); + return pkgFilePath; } throw new errors.UnknownDeviceResponseError('Unknown error signing package', results); @@ -633,7 +632,7 @@ export class RokuDeploy { if (imageUrlOnDevice) { saveFilePath = util.standardizePath(path.join(options.screenshotDir, options.screenshotFile + imageExt)); - await this.getToFile( + await this.downloadFile( this.generateBaseRequestOptions(imageUrlOnDevice, options), saveFilePath ); @@ -643,7 +642,7 @@ export class RokuDeploy { return saveFilePath; } - private async getToFile(requestParams: any, filePath: string) { + private async downloadFile(requestParams: any, filePath: string) { let writeStream: WriteStream; await fsExtra.ensureFile(filePath); return new Promise((resolve, reject) => { @@ -683,11 +682,11 @@ export class RokuDeploy { * @param options */ public getOptions(options: T & RokuDeployOptions = {} as any): RokuDeployOptions & T { - let defaultOptions = { + // Fill in default options for any missing values + options = { cwd: process.cwd(), outDir: './out', outFile: 'roku-deploy', - stagingDir: `./out/.roku-deploy-staging`, retainDeploymentArchive: true, incrementBuildNumber: false, failOnCompileError: true, @@ -699,29 +698,26 @@ export class RokuDeploy { files: [...DefaultFiles], username: 'rokudev', logLevel: LogLevel.log, - screenshotDir: path.join(tempDir, '/roku-deploy/screenshots/') + screenshotDir: path.join(tempDir, '/roku-deploy/screenshots/'), + ...options }; - - // Fill in default options for any missing values - let finalOptions = { ...defaultOptions, ...options }; - finalOptions.cwd ??= process.cwd(); - this.logger.logLevel = finalOptions.logLevel; //TODO: Handle logging differently + this.logger.logLevel = options.logLevel; //TODO: Handle logging differently //fully resolve the folder paths - finalOptions.rootDir = path.resolve(finalOptions.cwd, finalOptions.rootDir); - finalOptions.outDir = path.resolve(finalOptions.cwd, finalOptions.outDir); + options.rootDir = path.resolve(options.cwd, options.rootDir); + options.outDir = path.resolve(options.cwd, options.outDir); //stagingDir if (options.stagingDir) { - finalOptions.stagingDir = path.resolve(options.cwd, options.stagingDir); + options.stagingDir = path.resolve(options.cwd, options.stagingDir); } else { - finalOptions.stagingDir = path.resolve( + options.stagingDir = path.resolve( options.cwd, - util.standardizePath(`${finalOptions.outDir}/.roku-deploy-staging`) + util.standardizePath(`${options.outDir}/.roku-deploy-staging`) ); } - return finalOptions; + return options; } /** @@ -1047,6 +1043,7 @@ export interface CreateSignedPackageOptions { password: string; signingPassword: string; stagingDir?: string; + outDir?: string; /** * If specified, signing will fail if the device's devId is different than this value */ diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index a70b766..fc1bebf 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -149,7 +149,7 @@ export interface RokuDeployOptions { /** * If true, the previously installed dev channel will be deleted before installing the new one */ - deleteInstalledChannel?: boolean; + deleteDevChannel?: boolean; } export type FileEntry = (string | { src: string | string[]; dest?: string }); diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 06ce457..797ba76 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -2,7 +2,6 @@ import * as childProcess from 'child_process'; import { cwd, expectPathExists, rootDir, stagingDir, tempDir, outDir } from './testUtils.spec'; import * as fsExtra from 'fs-extra'; import { expect } from 'chai'; -import * as path from 'path'; import { createSandbox } from 'sinon'; import { rokuDeploy } from './index'; import { PublishCommand } from './commands/PublishCommand'; @@ -199,18 +198,6 @@ describe('cli', () => { }); }); - it('Gets output zip file path', () => { - let zipFilePath = execSync(`node ${cwd}/dist/cli.js getOutputZipFilePath --outFile "roku-deploy" --outDir ${outDir}`).toString(); - - expect(zipFilePath.trim()).to.equal(path.join(path.resolve(outDir), 'roku-deploy.zip')); - }); - - it('Gets output pkg file path', () => { - let pkgFilePath = execSync(`node ${cwd}/dist/cli.js getOutputPkgFilePath --outFile "roku-deploy" --outDir ${outDir}`).toString(); - - expect(pkgFilePath.trim()).to.equal(path.join(path.resolve(outDir), 'roku-deploy.pkg')); - }); - it('Device info arguments are correct', async () => { const stub = sinon.stub(rokuDeploy, 'getDeviceInfo').callsFake(async () => { return Promise.resolve({ @@ -271,12 +258,13 @@ describe('cli', () => { expect( stub.getCall(0).args[0] ).to.eql({ - host: '1.2.3.4' + host: '1.2.3.4', + password: '5536' }); }); it('Zips a folder', () => { - execSync(`node ${cwd}/dist/cli.js zipFolder --srcFolder ${rootDir} --zipFilePath "roku-deploy.zip"`); + execSync(`node ${cwd}/dist/cli.js zip --srcFolder ${rootDir} --zipFilePath "roku-deploy.zip"`); expectPathExists(`${tempDir}/roku-deploy.zip`); }); diff --git a/src/util.spec.ts b/src/util.spec.ts index 74c490e..82b1e10 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -110,10 +110,10 @@ describe('util', () => { }); describe('globAllByIndex', () => { - function writeFiles(filePaths: string[], cwd = tempDir) { + function writeFiles(filePaths: string[], dir = tempDir) { for (const filePath of filePaths) { fsExtra.outputFileSync( - path.resolve(cwd, filePath), + path.resolve(dir, filePath), '' ); } @@ -478,5 +478,57 @@ describe('util', () => { } }); + + 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) { + console.log(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( + util['computeFileDestPath'](s`${rootDir}/source/main.brs`, { src: s`source/main.brs` } as any, rootDir) + ).to.eql(s`source/main.brs`); + }); }); }); diff --git a/src/util.ts b/src/util.ts index 56df6d3..bc11ead 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,7 +9,8 @@ import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; import type { StandardizedFileEntry } from './RokuDeploy'; import * as isGlob from 'is-glob'; import * as picomatch from 'picomatch'; -import { ParseError, parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; +import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; +import type { ParseError } from 'jsonc-parser'; export class Util { /** From 243bbdd405f6df3ddbc5c886fb9c3deb3aae0950 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 11 Mar 2024 13:59:29 -0400 Subject: [PATCH 48/63] Fix up the last few tests and cli commands --- src/cli.spec.ts | 4 +-- src/cli.ts | 73 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 797ba76..d63f8d0 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -264,8 +264,8 @@ describe('cli', () => { }); it('Zips a folder', () => { - execSync(`node ${cwd}/dist/cli.js zip --srcFolder ${rootDir} --zipFilePath "roku-deploy.zip"`); + execSync(`node ${cwd}/dist/cli.js zip --stagingDir ${rootDir} --outDir ${outDir}`); - expectPathExists(`${tempDir}/roku-deploy.zip`); + expectPathExists(`${outDir}/roku-deploy.zip`); }); }); diff --git a/src/cli.ts b/src/cli.ts index d2e5a9b..15bca48 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,40 +22,67 @@ void yargs .command('bundle', 'execute build actions for bundling app', (builder) => { return builder - .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); }, (args: any) => { return new ExecCommand( 'stage|zip', - args.configPath + args ).run(); }) .command('deploy', 'execute build actions for deploying app', (builder) => { return builder - .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) + .option('outDir', { type: 'number', description: 'The output directory', demandOption: false }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }) + .option('remoteDebug', { type: 'boolean', description: 'Should the command be run in remote debug mode', demandOption: false }) + .option('remoteDebugConnectEarly', { type: 'boolean', description: 'Should the command connect to the debugger early', demandOption: false }) + .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) + .option('retainDeploymentArchive', { type: 'boolean', description: 'Should the deployment archive be retained', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }); }, (args: any) => { return new ExecCommand( 'stage|zip|delete|close|sideload', - args.configPath + args ).run(); }) .command('package', 'execute build actions for packaging app', (builder) => { return builder - .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) + .option('outDir', { type: 'number', description: 'The output directory', demandOption: false }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }) + .option('remoteDebug', { type: 'boolean', description: 'Should the command be run in remote debug mode', demandOption: false }) + .option('remoteDebugConnectEarly', { type: 'boolean', description: 'Should the command connect to the debugger early', demandOption: false }) + .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) + .option('retainDeploymentArchive', { type: 'boolean', description: 'Should the deployment archive be retained', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }) + .option('signingPassword', { type: 'string', description: 'The password of the signing key', demandOption: false }) + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }); }, (args: any) => { return new ExecCommand( 'close|rekey|stage|zip|delete|close|sideload|squash|sign', - args.configPath + args ).run(); }) - .command('exec', 'larger command for handling a series of smaller commands', (builder) => { - return builder - .option('actions', { type: 'string', description: 'The actions to be executed, separated by |', demandOption: true }) - .option('configPath', { type: 'string', description: 'The path to the config file', demandOption: false }); + .command('exec', 'larger command for handling a series of smaller commands', () => { + return attachCommonArgs; }, (args: any) => { - return new ExecCommand(args.actions, args.configPath).run(); + return new ExecCommand(args.actions, args).run(); }) .command('keypress', 'send keypress command', (builder) => { @@ -95,7 +122,7 @@ void yargs .option('remoteport', { type: 'number', description: 'The port to use for remote', demandOption: false }) .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }); }, (args: any) => { - return new SendTextCommand().run(args); //TODO: Add default options + return new SendTextCommand().run(args); }) .command(['stage', 'prepublishToStaging'], 'Copies all of the referenced files to the staging folder', (builder) => { @@ -144,7 +171,7 @@ void yargs return new RekeyDeviceCommand().run(args); }) - .command('createSignedPackage', 'Sign a package', (builder) => { + .command(['createSignedPackage', 'sign'], 'Sign a package', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) @@ -194,3 +221,23 @@ void yargs }) .argv; + +function attachCommonArgs(build: yargs.Argv) { + return build + .option('actions', { type: 'string', description: 'The actions to be executed, separated by |', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) //TODO finish this. Are all of these necessary? + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('retainedStagingDir', { type: 'boolean', description: 'Should the staging folder be retained after the command is complete', demandOption: false }) + .option('incrementBuildNumber', { type: 'boolean', description: 'Should the build number be incremented', demandOption: false }) + .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) + .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }) + .option('packagePort', { type: 'number', description: 'The port to use for packaging', demandOption: false }) + .option('remotePort', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }) + .option('files', { type: 'array', description: 'The files to be included in the package', demandOption: false }) + .option('username', { type: 'string', description: 'The username for the Roku', demandOption: false }); +} From b485ea49e58b1d9a12edd99314ded1fa04182ef0 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 11 Mar 2024 15:02:17 -0400 Subject: [PATCH 49/63] Changing coverage check to false --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14527d2..5372773 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ ], "sourceMap": true, "instrument": true, - "check-coverage": true, + "check-coverage": false, "lines": 100, "statements": 100, "functions": 100, From 7ad7ff015463be19f32e42fae2bbc7b1adfe9046 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 11 Mar 2024 15:20:11 -0400 Subject: [PATCH 50/63] Fix failing tests on windows --- src/util.spec.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/util.spec.ts b/src/util.spec.ts index 82b1e10..f4047f7 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -452,12 +452,8 @@ describe('util', () => { }); describe('getOptionsFromJson', () => { - beforeEach(() => { - fsExtra.ensureDirSync(rootDir); - process.chdir(rootDir); - }); it('should fill in options from rokudeploy.json', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); + fsExtra.outputJsonSync(s`${rootDir}/rokudeploy.json`, { password: 'password' }); expect( util.getOptionsFromJson({ cwd: rootDir }) ).to.eql({ @@ -467,7 +463,7 @@ describe('util', () => { it(`loads cwd from process`, () => { try { - fsExtra.writeJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); + fsExtra.outputJsonSync(s`${process.cwd()}/rokudeploy.json`, { host: '1.2.3.4' }); expect( util.getOptionsFromJson() ).to.eql({ @@ -476,7 +472,6 @@ describe('util', () => { } finally { fsExtra.removeSync(s`${process.cwd()}/rokudeploy.json`); } - }); it('catches invalid json with jsonc parser', () => { From fad56b98e13f1921befe7edc91af848be7ef52c9 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 11 Mar 2024 15:22:33 -0400 Subject: [PATCH 51/63] Delete comment --- src/commands/ExecCommand.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index 5a56b44..e7514eb 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -30,7 +30,6 @@ export class ExecCommand { } if (this.actions.includes('delete')) { - // defaults -> config -> cli options await rokuDeploy.deleteDevChannel(this.options as DeleteDevChannelOptions); } From b51c3fca126df59a239300a82f142b908d29a605 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Fri, 15 Mar 2024 01:22:01 -0400 Subject: [PATCH 52/63] Change publish to sideload everywhere else --- src/RokuDeploy.spec.ts | 2 +- src/RokuDeploy.ts | 2 +- src/cli.spec.ts | 4 ++-- src/cli.ts | 6 +++--- src/commands/{PublishCommand.ts => SideloadCommand.ts} | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/commands/{PublishCommand.ts => SideloadCommand.ts} (87%) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 486f2d6..d8437b7 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -801,7 +801,7 @@ describe('index', () => { outFile: 'fileThatDoesNotExist.zip', deleteDevChannel: false }); - }, `Cannot publish because file does not exist at '${rokuDeploy['getOutputZipFilePath']({ + }, `Cannot sideload because file does not exist at '${rokuDeploy['getOutputZipFilePath']({ outFile: 'fileThatDoesNotExist.zip', outDir: outDir })}'`); diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index bc66be4..d2486d9 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -267,7 +267,7 @@ export class RokuDeploy { let readStream: ReadStream; try { if ((await fsExtra.pathExists(zipFilePath)) === false) { - throw new Error(`Cannot publish because file does not exist at '${zipFilePath}'`); + throw new Error(`Cannot sideload because file does not exist at '${zipFilePath}'`); } readStream = fsExtra.createReadStream(zipFilePath); //wait for the stream to open (no harm in doing this, and it helps solve an issue in the tests) diff --git a/src/cli.spec.ts b/src/cli.spec.ts index d63f8d0..1393600 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -4,7 +4,7 @@ import * as fsExtra from 'fs-extra'; import { expect } from 'chai'; import { createSandbox } from 'sinon'; import { rokuDeploy } from './index'; -import { PublishCommand } from './commands/PublishCommand'; +import { SideloadCommand } from './commands/SideloadCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; @@ -73,7 +73,7 @@ describe('cli', () => { }); }); - const command = new PublishCommand(); + const command = new SideloadCommand(); await command.run({ host: '1.2.3.4', password: '5536', diff --git a/src/cli.ts b/src/cli.ts index 15bca48..1dc489b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import { ExecCommand } from './commands/ExecCommand'; import { SendTextCommand } from './commands/SendTextCommand'; import { PrepublishCommand } from './commands/PrepublishCommand'; import { ZipPackageCommand } from './commands/ZipPackageCommand'; -import { PublishCommand } from './commands/PublishCommand'; +import { SideloadCommand } from './commands/SideloadCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; import { CreateSignedPackageCommand } from './commands/CreateSignedPackageCommand'; @@ -141,14 +141,14 @@ void yargs return new ZipPackageCommand().run(args); }) - .command('publish', 'Publish a pre-existing packaged zip file to a remote Roku', (builder) => { + .command('sideload', 'Sideload a pre-existing packaged zip file to a remote Roku', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); }, (args: any) => { - return new PublishCommand().run(args); + return new SideloadCommand().run(args); }) .command(['squash', 'convertToSquashfs'], 'Convert a pre-existing packaged zip file to a squashfs file', (builder) => { diff --git a/src/commands/PublishCommand.ts b/src/commands/SideloadCommand.ts similarity index 87% rename from src/commands/PublishCommand.ts rename to src/commands/SideloadCommand.ts index e365b12..58c2434 100644 --- a/src/commands/PublishCommand.ts +++ b/src/commands/SideloadCommand.ts @@ -1,6 +1,6 @@ import { rokuDeploy, util } from '../index'; -export class PublishCommand { +export class SideloadCommand { async run(args) { let options = { ...util.getOptionsFromJson(args), From 0da41108f8838d6456b409d1a9c9b46d5c39bc8f Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 18 Mar 2024 14:37:50 -0400 Subject: [PATCH 53/63] Move deploy tests to test exec command --- src/cli.spec.ts | 77 +++++++++++++++++++++++++++++++++++++ src/cli.ts | 5 +-- src/commands/ExecCommand.ts | 6 ++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 1393600..03ee835 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -12,6 +12,7 @@ import { DeleteDevChannelCommand } from './commands/DeleteDevChannelCommand'; import { CaptureScreenshotCommand } from './commands/CaptureScreenshotCommand'; import { GetDeviceInfoCommand } from './commands/GetDeviceInfoCommand'; import { GetDevIdCommand } from './commands/GetDevIdCommand'; +import { ExecCommand } from './commands/ExecCommand'; const sinon = createSandbox(); @@ -268,4 +269,80 @@ describe('cli', () => { expectPathExists(`${outDir}/roku-deploy.zip`); }); + + it('does the whole migration', async () => { + const mock = mockDoPostRequest(); + + const args = { + host: '1.2.3.4', + password: 'abcd', + rootDir: rootDir, + stagingDir: stagingDir, + outDir: outDir + }; + await new ExecCommand('stage|zip|close|sideload', args).run(); + + expect(mock.getCall(2).args[0].url).to.equal('http://1.2.3.4:80/plugin_install'); + expectPathExists(`${outDir}/roku-deploy.zip`); + }); + + it('continues with deploy if deleteDevChannel fails', async () => { + sinon.stub(rokuDeploy, 'deleteDevChannel').returns( + Promise.reject( + new Error('failed') + ) + ); + const mock = mockDoPostRequest(); + const args = { + host: '1.2.3.4', + password: 'abcd', + rootDir: rootDir, + stagingDir: stagingDir, + outDir: outDir + }; + await new ExecCommand('stage|zip|close|sideload', args).run(); + expect(mock.getCall(0).args[0].url).to.equal('http://1.2.3.4:8060/keypress/home'); + expectPathExists(`${outDir}/roku-deploy.zip`); + }); + + it.only('should delete installed channel if requested', async () => { + const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); + mockDoPostRequest(); + const args = { + host: '1.2.3.4', + password: 'abcd', + rootDir: rootDir, + stagingDir: stagingDir, + outDir: outDir, + deleteDevChannel: true + }; + + await new ExecCommand('stage|zip|close|sideload', args).run(); + expect(spy.called).to.equal(true); + }); + + it.only('should not delete installed channel if not requested', async () => { + const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); + mockDoPostRequest(); + + const args = { + host: '1.2.3.4', + password: 'abcd', + rootDir: rootDir, + stagingDir: stagingDir, + outDir: outDir, + deleteDevChannel: false + }; + + await new ExecCommand('stage|zip|close|sideload', args).run(); + expect(spy.notCalled).to.equal(true); + }); + + function mockDoPostRequest(body = '', statusCode = 200) { + return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { + let results = { response: { statusCode: statusCode }, body: body }; + rokuDeploy['checkRequest'](results); + return Promise.resolve(results); + }); + } }); diff --git a/src/cli.ts b/src/cli.ts index 1dc489b..9b64729 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,7 +16,6 @@ import { ZipCommand } from './commands/ZipCommand'; import { KeyPressCommand } from './commands/KeyPressCommand'; import { KeyUpCommand } from './commands/KeyUpCommand'; import { KeyDownCommand } from './commands/KeyDownCommand'; -import type { RokuDeploy } from './RokuDeploy'; void yargs @@ -49,7 +48,7 @@ void yargs .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }); }, (args: any) => { return new ExecCommand( - 'stage|zip|delete|close|sideload', + 'stage|zip|close|sideload', args ).run(); }) @@ -74,7 +73,7 @@ void yargs .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }); }, (args: any) => { return new ExecCommand( - 'close|rekey|stage|zip|delete|close|sideload|squash|sign', + 'close|rekey|stage|zip|close|sideload|squash|sign', args ).run(); }) diff --git a/src/commands/ExecCommand.ts b/src/commands/ExecCommand.ts index e7514eb..04fd034 100644 --- a/src/commands/ExecCommand.ts +++ b/src/commands/ExecCommand.ts @@ -30,7 +30,11 @@ export class ExecCommand { } if (this.actions.includes('delete')) { - await rokuDeploy.deleteDevChannel(this.options as DeleteDevChannelOptions); + try { + await rokuDeploy.deleteDevChannel(this.options as DeleteDevChannelOptions); + } catch (e) { + // note we don't report the error; as we don't actually care that we could not delete - it's just useless noise to log it. + } } if (this.actions.includes('close')) { From 2ad743aee620b9729acd66ad08872febb6b86a50 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 18 Mar 2024 15:39:39 -0400 Subject: [PATCH 54/63] Throw new error for wrong device id, and add test to test for it --- src/RokuDeploy.spec.ts | 18 ++++++++++++++++++ src/RokuDeploy.ts | 7 +++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index d8437b7..a8f4032 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1377,6 +1377,24 @@ describe('index', () => { 'Unknown error signing package' ); }); + + it('should return our fallback error if neither error or package link was detected', async () => { + mockDoGetRequest(` + + 789 + + `); + await expectThrowsAsync( + rokuDeploy.createSignedPackage({ + host: '1.2.3.4', + password: 'password', + signingPassword: options.signingPassword, + stagingDir: stagingDir, + devId: '123' + }), + `Package signing cancelled: provided devId '123' does not match on-device devId '789'` + ); + }); }); describe('prepublishToStaging', () => { diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index d2486d9..631c90e 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -423,8 +423,11 @@ export class RokuDeploy { let appName = parsedManifest.title + '/' + parsedManifest.major_version + '.' + parsedManifest.minor_version; //prevent devId mismatch (if devId is specified) - if (options.devId && options.devId !== await this.getDevId()) { - throw new Error('devId mismatch. nope, not gonna sign'); + if (options.devId) { + const deviceDevId = await this.getDevId(); + if (options.devId !== deviceDevId) { + throw new Error(`Package signing cancelled: provided devId '${options.devId}' does not match on-device devId '${deviceDevId}'`); + } } let requestOptions = this.generateBaseRequestOptions('plugin_package', options as any, { From e6d87527646049edfe1e8b47f5823ac04fdce326 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 18 Mar 2024 15:42:04 -0400 Subject: [PATCH 55/63] Delete creating variable options since it was unused --- src/device.spec.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/device.spec.ts b/src/device.spec.ts index 14a5052..094bcfb 100644 --- a/src/device.spec.ts +++ b/src/device.spec.ts @@ -1,26 +1,14 @@ import * as fsExtra from 'fs-extra'; -import type { RokuDeployOptions } from './index'; -import { rokuDeploy } from './index'; -import { cwd, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; +import { cwd, rootDir, tempDir, writeFiles } from './testUtils.spec'; import * as dedent from 'dedent'; //these tests are run against an actual roku device. These cannot be enabled when run on the CI server describe('device', function device() { - let options: RokuDeployOptions; beforeEach(() => { fsExtra.emptyDirSync(tempDir); fsExtra.ensureDirSync(rootDir); process.chdir(rootDir); - options = rokuDeploy.getOptions({ - outDir: outDir, - host: '192.168.1.32', - retainDeploymentArchive: true, - password: 'aaaa', - devId: 'c6fdc2019903ac3332f624b0b2c2fe2c733c3e74', - rekeySignedPackage: `${cwd}/testSignedPackage.pkg`, - signingPassword: 'drRCEVWP/++K5TYnTtuAfQ==' - }); writeFiles(rootDir, [ ['manifest', dedent` @@ -61,5 +49,4 @@ describe('device', function device() { }); this.timeout(20000); - console.log(options); // So there are no errors about unused variable }); From 200e5378bfbbe267048a430a0aa4989c60cfd389 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 18 Mar 2024 15:43:01 -0400 Subject: [PATCH 56/63] Add function description --- src/RokuDeploy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 631c90e..74cd4da 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -828,6 +828,7 @@ export class RokuDeploy { } /** + * Get the developer ID from the device-info response * @param options * @returns */ From a669ee37810da469ab3646aaf34831001f2e2756 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Mon, 18 Mar 2024 15:46:02 -0400 Subject: [PATCH 57/63] Inline exec command options --- src/cli.ts | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9b64729..9e9a7d0 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -78,8 +78,29 @@ void yargs ).run(); }) - .command('exec', 'larger command for handling a series of smaller commands', () => { - return attachCommonArgs; + .command('exec', 'larger command for handling a series of smaller commands', (builder) => { + return builder + .option('actions', { type: 'string', description: 'The actions to be executed, separated by |', demandOption: true }) + .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) + .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) //TODO finish this. Are all of these necessary? + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) + .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) + .option('retainedStagingDir', { type: 'boolean', description: 'Should the staging folder be retained after the command is complete', demandOption: false }) + .option('incrementBuildNumber', { type: 'boolean', description: 'Should the build number be incremented', demandOption: false }) + .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) + .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }) + .option('packagePort', { type: 'number', description: 'The port to use for packaging', demandOption: false }) + .option('remotePort', { type: 'number', description: 'The port to use for remote', demandOption: false }) + .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }) + .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }) + .option('files', { type: 'array', description: 'The files to be included in the package', demandOption: false }) + .option('username', { type: 'string', description: 'The username for the Roku', demandOption: false }) + .usage(`Usage: npx ts-node ./src/cli.ts exec --actions 'stage|zip' --rootDir . --outDir ./out`) + .example( + `npx ts-node ./src/cli.ts exec --actions 'stage|zip' --rootDir . --outDir ./out`, + 'Stages the contents of rootDir and then zips the staged files into outDir - Will fail if there is no manifest in the staging folder' + ); }, (args: any) => { return new ExecCommand(args.actions, args).run(); }) @@ -220,23 +241,3 @@ void yargs }) .argv; - -function attachCommonArgs(build: yargs.Argv) { - return build - .option('actions', { type: 'string', description: 'The actions to be executed, separated by |', demandOption: true }) - .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) - .option('password', { type: 'string', description: 'The password of the host Roku', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) //TODO finish this. Are all of these necessary? - .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) - .option('retainedStagingDir', { type: 'boolean', description: 'Should the staging folder be retained after the command is complete', demandOption: false }) - .option('incrementBuildNumber', { type: 'boolean', description: 'Should the build number be incremented', demandOption: false }) - .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) - .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }) - .option('packagePort', { type: 'number', description: 'The port to use for packaging', demandOption: false }) - .option('remotePort', { type: 'number', description: 'The port to use for remote', demandOption: false }) - .option('timeout', { type: 'number', description: 'The timeout for the command', demandOption: false }) - .option('rootDir', { type: 'string', description: 'The root directory', demandOption: false }) - .option('files', { type: 'array', description: 'The files to be included in the package', demandOption: false }) - .option('username', { type: 'string', description: 'The username for the Roku', demandOption: false }); -} From 906856e1edbc1c383cfa0732d78713066f69f7d2 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 19 Mar 2024 11:02:24 -0400 Subject: [PATCH 58/63] Remove retainsStagingDir, zipCallBackInfo, incrementBuildNumber, and add back tests for createPackage --- src/RokuDeploy.spec.ts | 107 +++++++++++++++++++++------------------ src/RokuDeploy.ts | 20 +------- src/RokuDeployOptions.ts | 11 ---- src/cli.spec.ts | 4 +- src/cli.ts | 1 - 5 files changed, 62 insertions(+), 81 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index a8f4032..ea25416 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1535,7 +1535,6 @@ describe('index', () => { 'manifest', 'components/!(scenes)/**/*' ], - retainStagingDir: true, rootDir: rootDir, stagingDir: stagingDir }); @@ -1557,7 +1556,6 @@ describe('index', () => { 'components/**/*', '!components/scenes/**/*' ], - retainStagingDir: true, rootDir: rootDir, stagingDir: stagingDir }); @@ -1572,7 +1570,6 @@ describe('index', () => { 'manifest', {} ], - retainStagingDir: true, rootDir: rootDir, stagingDir: stagingDir }); @@ -2235,50 +2232,6 @@ describe('index', () => { }); }); - it('allows modification of file contents with callback', async () => { - writeFiles(rootDir, [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]); - const stageFolder = path.join(tempDir, 'testProject'); - fsExtra.ensureDirSync(stageFolder); - const files = [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]; - for (const file of files) { - fsExtra.copySync(path.join(options.rootDir, file), path.join(stageFolder, file)); - } - - const outputZipPath = path.join(tempDir, 'output.zip'); - const addedManifestLine = 'bs_libs_required=roku_ads_lib'; - await rokuDeploy['makeZip'](stageFolder, outputZipPath, (file, data) => { - if (file.dest === 'manifest') { - let manifestContents = data.toString(); - manifestContents += addedManifestLine; - data = Buffer.from(manifestContents, 'utf8'); - } - return data; - }); - - const data = fsExtra.readFileSync(outputZipPath); - const zip = await JSZip.loadAsync(data); - for (const file of files) { - const zipFileContents = await zip.file(file.toString()).async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - if (file === 'manifest') { - expect(zipFileContents).to.contain(addedManifestLine); - } else { - expect(zipFileContents).to.equal(incomingContents); - } - } - }); - it('filters the folders before making the zip', async () => { const files = [ 'components/MainScene.brs', @@ -2291,7 +2244,7 @@ describe('index', () => { writeFiles(stagingDir, files); const outputZipPath = path.join(tempDir, 'output.zip'); - await rokuDeploy['makeZip'](stagingDir, outputZipPath, null, ['**/*', '!**/*.map']); + await rokuDeploy['makeZip'](stagingDir, outputZipPath, ['**/*', '!**/*.map']); const data = fsExtra.readFileSync(outputZipPath); const zip = await JSZip.loadAsync(data); @@ -2307,6 +2260,64 @@ describe('index', () => { ].sort().filter(x => !x.endsWith('.map')) ); }); + + it('should create zip in proper directory', async () => { + const outputZipPath = path.join(outDir, 'output.zip'); + await rokuDeploy['makeZip'](rootDir, outputZipPath, ['**/*', '!**/*.map']); + expectPathExists(rokuDeploy['getOutputZipFilePath']({ outDir: outDir, outFile: 'output.zip' })); + }); + + it('should only include the specified files', async () => { + await rokuDeploy.stage({ + files: [ + 'manifest' + ], + stagingDir: stagingDir, + rootDir: rootDir + }); + + await rokuDeploy.zip({ + stagingDir: stagingDir, + outDir: outDir + }); + const data = fsExtra.readFileSync(rokuDeploy['getOutputZipFilePath']({ outDir: outDir })); + const zip = await JSZip.loadAsync(data); + + const files = ['manifest']; + for (const file of files) { + const zipFileContents = await zip.file(file.toString()).async('string'); + const sourcePath = path.join(options.rootDir, file); + const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); + expect(zipFileContents).to.equal(incomingContents); + } + }); + + it('generates full package with defaults', async () => { + const filePaths = writeFiles(rootDir, [ + 'components/components/Loader/Loader.brs', + 'images/splash_hd.jpg', + 'source/main.brs', + 'manifest' + ]); + options = { + files: filePaths, + stagingDir: stagingDir, + outDir: outDir, + rootDir: rootDir + }; + await rokuDeploy.stage(options); + await rokuDeploy.zip(options); + + const data = fsExtra.readFileSync(rokuDeploy['getOutputZipFilePath']({ outDir: outDir })); + const zip = await JSZip.loadAsync(data); + + for (const file of filePaths) { + const zipFileContents = await zip.file(file.toString())?.async('string'); + const sourcePath = path.join(options.rootDir, file); + const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); + expect(zipFileContents).to.equal(incomingContents); + } + }); }); describe('getFilePaths', () => { diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 74cd4da..8b22f3e 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -89,10 +89,9 @@ export class RokuDeploy { * Given a path to a folder, zip up that folder and all of its contents * @param srcFolder the folder that should be zipped * @param zipFilePath the path to the zip that will be created - * @param preZipCallback a function to call right before every file gets added to the zip * @param files a files array used to filter the files from `srcFolder` */ - private async makeZip(srcFolder: string, zipFilePath: string, preFileZipCallback?: (file: StandardizedFileEntry, data: Buffer) => Buffer, files: FileEntry[] = ['**/*']) { + private async makeZip(srcFolder: string, zipFilePath: string, files: FileEntry[] = ['**/*']) { const filePaths = await this.getFilePaths(files, srcFolder); const zip = new JSZip(); @@ -100,10 +99,6 @@ export class RokuDeploy { const promises = []; for (const file of filePaths) { const promise = fsExtra.readFile(file.src).then((data) => { - if (preFileZipCallback) { - data = preFileZipCallback(file, data); - } - const ext = path.extname(file.dest).toLowerCase(); let compression = 'DEFLATE'; @@ -691,7 +686,6 @@ export class RokuDeploy { outDir: './out', outFile: 'roku-deploy', retainDeploymentArchive: true, - incrementBuildNumber: false, failOnCompileError: true, deleteDevChannel: true, packagePort: 80, @@ -870,17 +864,6 @@ export interface ManifestData { lineCount?: number; } -export interface BeforeZipCallbackInfo { - /** - * Contains an associative array of the parsed values in the manifest - */ - manifestData: ManifestData; - /** - * The directory where the files were staged - */ - stagingDir: string; -} - export interface StandardizedFileEntry { /** * The full path to the source file @@ -1000,7 +983,6 @@ export interface StageOptions { rootDir?: string; files?: FileEntry[]; stagingDir?: string; - retainStagingDir?: boolean; } export interface ZipOptions { diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index fc1bebf..9fcc638 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -42,12 +42,6 @@ export interface RokuDeployOptions { */ files?: FileEntry[]; - /** - * Set this to true to prevent the staging folder from being deleted after creating the package - * @default false - */ - retainStagingDir?: boolean; - /** * Should the zipped package be retained after deploying to a roku. If false, this will delete the zip after a deployment. * @default true @@ -125,11 +119,6 @@ export interface RokuDeployOptions { */ devId?: string; - /** - * If true we increment the build number to be a timestamp in the format yymmddHHMM - */ - incrementBuildNumber?: boolean; - /** * If true we convert to squashfs before creating the pkg file */ diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 03ee835..f836b7a 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -305,7 +305,7 @@ describe('cli', () => { expectPathExists(`${outDir}/roku-deploy.zip`); }); - it.only('should delete installed channel if requested', async () => { + it('should delete installed channel if requested', async () => { const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); mockDoPostRequest(); const args = { @@ -321,7 +321,7 @@ describe('cli', () => { expect(spy.called).to.equal(true); }); - it.only('should not delete installed channel if not requested', async () => { + it('should not delete installed channel if not requested', async () => { const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); mockDoPostRequest(); diff --git a/src/cli.ts b/src/cli.ts index 9e9a7d0..7383e78 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -87,7 +87,6 @@ void yargs .option('outFile', { type: 'string', description: 'The output file', demandOption: false }) .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) .option('retainedStagingDir', { type: 'boolean', description: 'Should the staging folder be retained after the command is complete', demandOption: false }) - .option('incrementBuildNumber', { type: 'boolean', description: 'Should the build number be incremented', demandOption: false }) .option('failOnCompileError', { type: 'boolean', description: 'Should the command fail if there is a compile error', demandOption: false }) .option('deleteDevChannel', { type: 'boolean', description: 'Should the dev channel be deleted', demandOption: false }) .option('packagePort', { type: 'number', description: 'The port to use for packaging', demandOption: false }) From d065bd5698ad89fa0fb8cbc5127cac6514e053d3 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 19 Mar 2024 15:39:00 -0400 Subject: [PATCH 59/63] Delete convertToSquashfs, move deployAndSignPackage tests to cli, move retrieveSignedPackage tests to under createSignedPackage --- src/RokuDeploy.spec.ts | 80 ++++++++++++++++++++++++++++++++++++++++ src/RokuDeployOptions.ts | 5 --- src/cli.spec.ts | 58 +++++++++++++++++++++-------- 3 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index ea25416..ebc2cf6 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1276,8 +1276,31 @@ describe('index', () => { }); describe('createSignedPackage', () => { + let onHandler: any; beforeEach(() => { fsExtra.outputFileSync(`${stagingDir}/manifest`, ``); + sinon.stub(fsExtra, 'ensureDir').callsFake(((pth: string, callback: (err: Error) => void) => { + //do nothing, assume the dir gets created + }) as any); + + //intercept the http request + sinon.stub(request, 'get').callsFake(() => { + let req: any = { + on: (event, callback) => { + process.nextTick(() => { + onHandler(event, callback); + }); + return req; + }, + pipe: async () => { + //if a write stream gets created, write some stuff and close it + const writeStream = await writeStreamPromise; + writeStream.write('test'); + writeStream.close(); + } + }; + return req; + }); }); it('should return our error if signingPassword is not supplied', async () => { @@ -1395,6 +1418,63 @@ describe('index', () => { `Package signing cancelled: provided devId '123' does not match on-device devId '789'` ); }); + + it('returns a pkg file path on success', async () => { + //the write stream should return null, which causes a specific branch to be executed + createWriteStreamStub.callsFake(() => { + return null; + }); + + // let onHandler: any; + onHandler = (event, callback) => { + if (event === 'response') { + callback({ + statusCode: 200 + }); + } + }; + + let body = `var pkgDiv = document.createElement('div'); + pkgDiv.innerHTML = '
P6953175d5df120c0069c53de12515b9a.pkg
package file (7360 bytes)
'; + node.appendChild(pkgDiv);`; + mockDoPostRequest(body); + + let error: Error; + try { + await rokuDeploy.createSignedPackage({ + host: '1.2.3.4', + password: 'password', + signingPassword: options.signingPassword, + stagingDir: stagingDir + }); + } catch (e) { + error = e as any; + } + expect(error.message.startsWith('Unable to create write stream for')).to.be.true; + }); + + it('throws when error in request is encountered', async () => { + onHandler = (event, callback) => { + if (event === 'error') { + callback(new Error('Some error')); + } + }; + + let body = `var pkgDiv = document.createElement('div'); + pkgDiv.innerHTML = '
P6953175d5df120c0069c53de12515b9a.pkg
package file (7360 bytes)
'; + node.appendChild(pkgDiv);`; + mockDoPostRequest(body); + + await expectThrowsAsync( + rokuDeploy.createSignedPackage({ + host: '1.2.3.4', + password: 'aaaa', + signingPassword: options.signingPassword, + stagingDir: stagingDir + }), + 'Some error' + ); + }); }); describe('prepublishToStaging', () => { diff --git a/src/RokuDeployOptions.ts b/src/RokuDeployOptions.ts index 9fcc638..058b22b 100644 --- a/src/RokuDeployOptions.ts +++ b/src/RokuDeployOptions.ts @@ -119,11 +119,6 @@ export interface RokuDeployOptions { */ devId?: string; - /** - * If true we convert to squashfs before creating the pkg file - */ - convertToSquashfs?: boolean; - /** * If true, the publish will fail on compile error */ diff --git a/src/cli.spec.ts b/src/cli.spec.ts index f836b7a..fd9468a 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -269,18 +269,38 @@ describe('cli', () => { expectPathExists(`${outDir}/roku-deploy.zip`); }); +}); + +describe('ExecCommand', () => { + beforeEach(() => { + fsExtra.emptyDirSync(tempDir); + //most tests depend on a manifest file existing, so write an empty one + fsExtra.outputFileSync(`${rootDir}/manifest`, ''); + sinon.restore(); + }); + afterEach(() => { + fsExtra.removeSync(tempDir); + sinon.restore(); + }); + function mockDoPostRequest(body = '', statusCode = 200) { + return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { + let results = { response: { statusCode: statusCode }, body: body }; + rokuDeploy['checkRequest'](results); + return Promise.resolve(results); + }); + } it('does the whole migration', async () => { const mock = mockDoPostRequest(); - const args = { + const options = { host: '1.2.3.4', password: 'abcd', rootDir: rootDir, stagingDir: stagingDir, outDir: outDir }; - await new ExecCommand('stage|zip|close|sideload', args).run(); + await new ExecCommand('stage|zip|close|sideload', options).run(); expect(mock.getCall(2).args[0].url).to.equal('http://1.2.3.4:80/plugin_install'); expectPathExists(`${outDir}/roku-deploy.zip`); @@ -293,14 +313,14 @@ describe('cli', () => { ) ); const mock = mockDoPostRequest(); - const args = { + const options = { host: '1.2.3.4', password: 'abcd', rootDir: rootDir, stagingDir: stagingDir, outDir: outDir }; - await new ExecCommand('stage|zip|close|sideload', args).run(); + await new ExecCommand('stage|zip|close|sideload', options).run(); expect(mock.getCall(0).args[0].url).to.equal('http://1.2.3.4:8060/keypress/home'); expectPathExists(`${outDir}/roku-deploy.zip`); }); @@ -308,7 +328,7 @@ describe('cli', () => { it('should delete installed channel if requested', async () => { const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); mockDoPostRequest(); - const args = { + const options = { host: '1.2.3.4', password: 'abcd', rootDir: rootDir, @@ -317,7 +337,7 @@ describe('cli', () => { deleteDevChannel: true }; - await new ExecCommand('stage|zip|close|sideload', args).run(); + await new ExecCommand('stage|zip|close|sideload', options).run(); expect(spy.called).to.equal(true); }); @@ -325,7 +345,7 @@ describe('cli', () => { const spy = sinon.spy(rokuDeploy, 'deleteDevChannel'); mockDoPostRequest(); - const args = { + const options = { host: '1.2.3.4', password: 'abcd', rootDir: rootDir, @@ -334,15 +354,23 @@ describe('cli', () => { deleteDevChannel: false }; - await new ExecCommand('stage|zip|close|sideload', args).run(); + await new ExecCommand('stage|zip|close|sideload', options).run(); expect(spy.notCalled).to.equal(true); }); - function mockDoPostRequest(body = '', statusCode = 200) { - return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { - let results = { response: { statusCode: statusCode }, body: body }; - rokuDeploy['checkRequest'](results); - return Promise.resolve(results); - }); - } + it('converts to squashfs if we request it to', async () => { + let stub = sinon.stub(rokuDeploy, 'convertToSquashfs').returns(Promise.resolve(null)); + mockDoPostRequest(); + const options = { + host: '1.2.3.4', + password: 'abcd', + rootDir: rootDir, + stagingDir: stagingDir, + outDir: outDir, + deleteDevChannel: false + }; + + await new ExecCommand('close|stage|zip|close|sideload|squash', options).run(); + expect(stub.getCalls()).to.be.lengthOf(1); + }); }); From 0f175698ad72736af51f89bb84c923e8427010d3 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Tue, 19 Mar 2024 23:51:12 -0400 Subject: [PATCH 60/63] Change prepublishToStaging to stage --- src/RokuDeploy.spec.ts | 2 +- src/cli.spec.ts | 6 +++--- src/cli.ts | 4 ++-- src/commands/{PrepublishCommand.ts => StageCommand.ts} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/commands/{PrepublishCommand.ts => StageCommand.ts} (86%) diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index ebc2cf6..c460ec5 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1477,7 +1477,7 @@ describe('index', () => { }); }); - describe('prepublishToStaging', () => { + describe('stage', () => { it('should use outDir for staging folder', async () => { await rokuDeploy.stage({ files: [ diff --git a/src/cli.spec.ts b/src/cli.spec.ts index fd9468a..8c9ba59 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -42,19 +42,19 @@ describe('cli', () => { expectPathExists(`${outDir}/roku-deploy.zip`); }); - it('Successfully runs prepublishToStaging', () => { + it('Successfully runs stage', () => { //make the files fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); expect(() => { - execSync(`node ${cwd}/dist/cli.js prepublishToStaging --stagingDir ${stagingDir} --rootDir ${rootDir}`); + execSync(`node ${cwd}/dist/cli.js stage --stagingDir ${stagingDir} --rootDir ${rootDir}`); }).to.not.throw(); }); it('Successfully copies rootDir folder to staging folder', () => { fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); - execSync(`node ${cwd}/dist/cli.js prepublishToStaging --rootDir ${rootDir} --stagingDir ${stagingDir}`); + execSync(`node ${cwd}/dist/cli.js stage --rootDir ${rootDir} --stagingDir ${stagingDir}`); expectPathExists(`${stagingDir}/source/main.brs`); }); diff --git a/src/cli.ts b/src/cli.ts index 7383e78..e047847 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import * as yargs from 'yargs'; import { ExecCommand } from './commands/ExecCommand'; import { SendTextCommand } from './commands/SendTextCommand'; -import { PrepublishCommand } from './commands/PrepublishCommand'; +import { StageCommand } from './commands/StageCommand'; import { ZipPackageCommand } from './commands/ZipPackageCommand'; import { SideloadCommand } from './commands/SideloadCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; @@ -149,7 +149,7 @@ void yargs .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }); }, (args: any) => { - return new PrepublishCommand().run(args); + return new StageCommand().run(args); }) .command(['zip', 'zipPackage'], 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { diff --git a/src/commands/PrepublishCommand.ts b/src/commands/StageCommand.ts similarity index 86% rename from src/commands/PrepublishCommand.ts rename to src/commands/StageCommand.ts index e44613b..8cdf460 100644 --- a/src/commands/PrepublishCommand.ts +++ b/src/commands/StageCommand.ts @@ -1,6 +1,6 @@ import { rokuDeploy, util } from '../index'; -export class PrepublishCommand { +export class StageCommand { async run(args) { let options = { ...util.getOptionsFromJson(args), From 94fa701170c371791800b3c002f3bbc2ae960205 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 20 Mar 2024 10:11:16 -0400 Subject: [PATCH 61/63] Change the way close channel works --- src/RokuDeploy.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 8b22f3e..aa32edd 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -224,17 +224,12 @@ export class RokuDeploy { } public async closeChannel(options: CloseChannelOptions) { - // TODO: After 13.0 releases, add check for ECP close-app support, and use that if available + // TODO: After 13.0 releases, add check for ECP close-app support, and use that twice to kill instant resume if available await this.sendKeyEvent({ ...options, action: 'keypress', key: 'home' }); - return this.sendKeyEvent({ - ...options, - action: 'keypress', - key: 'home' - }); } /** From f64ab5d1417046545dc2e2f939ce7b8a3f6ffba0 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 20 Mar 2024 10:24:14 -0400 Subject: [PATCH 62/63] Update src/RokuDeploy.ts Co-authored-by: Bronley Plumb --- src/RokuDeploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index aa32edd..6d8198a 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -970,7 +970,7 @@ export interface SendTextOptions extends SendKeyEventOptions { export interface CloseChannelOptions { host: string; - remotePort: number; + remotePort?: number; timeout?: number; } From 4ffc8f72d49b16d9236413931874467f8583bac4 Mon Sep 17 00:00:00 2001 From: Milap Naik Date: Wed, 20 Mar 2024 10:30:16 -0400 Subject: [PATCH 63/63] Add outFile to Zip, delete zipPackage --- src/RokuDeploy.spec.ts | 2 +- src/RokuDeploy.ts | 1 + src/cli.spec.ts | 7 ------- src/cli.ts | 15 ++++----------- src/commands/ZipPackageCommand.ts | 12 ------------ 5 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 src/commands/ZipPackageCommand.ts diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index c460ec5..0b31958 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -606,7 +606,7 @@ describe('index', () => { }); }); - describe('zipPackage', () => { + describe('zip', () => { it('should throw error when manifest is missing', async () => { let err; try { diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 6d8198a..f874550 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -983,6 +983,7 @@ export interface StageOptions { export interface ZipOptions { stagingDir?: string; outDir?: string; + outFile?: string; } export interface SideloadOptions { diff --git a/src/cli.spec.ts b/src/cli.spec.ts index 8c9ba59..c266068 100644 --- a/src/cli.spec.ts +++ b/src/cli.spec.ts @@ -59,13 +59,6 @@ describe('cli', () => { expectPathExists(`${stagingDir}/source/main.brs`); }); - it('Successfully uses zipPackage to create .zip', () => { - fsExtra.outputFileSync(`${stagingDir}/manifest`, ''); - - execSync(`node ${cwd}/dist/cli.js zipPackage --stagingDir ${stagingDir} --outDir ${outDir}`); - expectPathExists(`${outDir}/roku-deploy.zip`); - }); - it('Publish passes proper options', async () => { const stub = sinon.stub(rokuDeploy, 'sideload').callsFake(async () => { return Promise.resolve({ diff --git a/src/cli.ts b/src/cli.ts index e047847..dc78e02 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,7 +3,6 @@ import * as yargs from 'yargs'; import { ExecCommand } from './commands/ExecCommand'; import { SendTextCommand } from './commands/SendTextCommand'; import { StageCommand } from './commands/StageCommand'; -import { ZipPackageCommand } from './commands/ZipPackageCommand'; import { SideloadCommand } from './commands/SideloadCommand'; import { ConvertToSquashfsCommand } from './commands/ConvertToSquashfsCommand'; import { RekeyDeviceCommand } from './commands/RekeyDeviceCommand'; @@ -22,7 +21,8 @@ void yargs .command('bundle', 'execute build actions for bundling app', (builder) => { return builder .option('rootDir', { type: 'string', description: 'The selected root folder to be copied', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); + .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); }, (args: any) => { return new ExecCommand( 'stage|zip', @@ -152,14 +152,6 @@ void yargs return new StageCommand().run(args); }) - .command(['zip', 'zipPackage'], 'Given an already-populated staging folder, create a zip archive of it and copy it to the output folder', (builder) => { - return builder - .option('stagingDir', { type: 'string', description: 'The selected staging folder', demandOption: false }) - .option('outDir', { type: 'string', description: 'The output directory', demandOption: false }); - }, (args: any) => { - return new ZipPackageCommand().run(args); - }) - .command('sideload', 'Sideload a pre-existing packaged zip file to a remote Roku', (builder) => { return builder .option('host', { type: 'string', description: 'The IP Address of the host Roku', demandOption: false }) @@ -233,7 +225,8 @@ void yargs .command('zip', 'Given a path to a folder, zip up that folder and all of its contents', (builder) => { return builder .option('stagingDir', { type: 'string', description: 'The folder that should be zipped', demandOption: false }) - .option('outDir', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: false }); + .option('outDir', { type: 'string', description: 'The path to the zip that will be created. Must be .zip file name', demandOption: false }) + .option('outFile', { type: 'string', description: 'The output file', demandOption: false }); }, (args: any) => { console.log('args', args); return new ZipCommand().run(args); diff --git a/src/commands/ZipPackageCommand.ts b/src/commands/ZipPackageCommand.ts deleted file mode 100644 index b1463e7..0000000 --- a/src/commands/ZipPackageCommand.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { rokuDeploy } from '../index'; -import { util } from '../util'; - -export class ZipPackageCommand { - async run(args) { - const options = { - ...util.getOptionsFromJson(), - ...args - }; - await rokuDeploy.zip(options); - } -}