From 7797f83bc8fb460d29b9112bfb30ff2035548a53 Mon Sep 17 00:00:00 2001 From: Daniel Huber Date: Fri, 19 Nov 2021 20:16:20 +0100 Subject: [PATCH] feat: add support for nodecg-io 0.2 (#39) * feat: add support for nodecg-io 0.2 * Fix test compile errors due to unchecked array access --- README.md | 1 + src/install/index.ts | 5 +++-- src/install/prompt.ts | 11 ++++++----- src/nodecgIOVersions.ts | 34 +++++++++++++++++++++++++++++---- src/utils/nodecgInstallation.ts | 7 ++++++- test/install/development.ts | 4 ++-- test/install/prompt.ts | 4 ++-- test/uninstall/index.ts | 12 ++++++------ test/utils/fs.ts | 4 ++-- test/utils/npm/npmVersion.ts | 2 +- test/utils/npm/pkgManagement.ts | 24 +++++++++++------------ tsconfig.json | 1 + 12 files changed, 72 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 349db8d..0b8491c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The following table show which versions of the CLI are compatible with which nod | CLI versions | nodecg-io versions | | ------------ | ------------------ | | `0.1` | `0.1` | +| `0.2` | `0.2`, `0.1` | Currently, they are the same, but we will follow [semver2](https://semver.org/) using [semantic-release](https://semantic-release.gitbook.io/semantic-release/) and the versions will diverge at some point. diff --git a/src/install/index.ts b/src/install/index.ts index 8ddb267..b229390 100644 --- a/src/install/index.ts +++ b/src/install/index.ts @@ -32,13 +32,14 @@ async function install(): Promise { logger.debug(`Detected nodecg installation at ${nodecgDir}.`); const nodecgIODir = getNodeCGIODirectory(nodecgDir); - const currentInstall = await readInstallInfo(nodecgIODir); + let currentInstall = await readInstallInfo(nodecgIODir); const requestedInstall = await promptForInstallInfo(currentInstall); // If the minor version changed and we already have another one installed, we need to delete it, so it can be properly installed. if (currentInstall && currentInstall.version !== requestedInstall.version && (await directoryExists(nodecgIODir))) { logger.info(`Deleting nodecg-io version ${currentInstall.version}...`); await removeDirectory(nodecgIODir); + currentInstall = undefined; } logger.info(`Installing nodecg-io version ${requestedInstall.version}...`); @@ -56,7 +57,7 @@ async function install(): Promise { // Add bundle dirs to the nodecg config, so that they are loaded. await manageBundleDir(nodecgDir, nodecgIODir, true); - await manageBundleDir(nodecgDir, path.join(nodecgIODir, "services"), requestedInstall.version !== "0.1"); + await manageBundleDir(nodecgDir, path.join(nodecgIODir, "services"), requestedInstall.version === "development"); await manageBundleDir( nodecgDir, path.join(nodecgIODir, "samples"), diff --git a/src/install/prompt.ts b/src/install/prompt.ts index c11f1e7..a84355d 100644 --- a/src/install/prompt.ts +++ b/src/install/prompt.ts @@ -40,7 +40,7 @@ export async function promptForInstallInfo(currentInstall: Installation | undefi message: "Which version do you want to install?", choices: [...versions, developmentVersion].reverse(), loop: false, - default: currentInstall?.version ?? versions.slice(-1)[0], + default: currentInstall?.version ?? versions[versions.length - 1], }, // Options for development installs { @@ -131,7 +131,7 @@ export async function buildPackageList(version: string, services: string[]): Pro name: pkgName, path: getPackagePath(pkgName), version: await getPackageVersion(pkgName, version), - symlink: getPackageSymlinks(pkgName), + symlink: getPackageSymlinks(version, pkgName), })); return await Promise.all(resolvePromises); @@ -154,9 +154,10 @@ async function getPackageVersion(pkgName: string, minorVersion: string) { return version?.version ?? `${minorVersion}.0`; } -function getPackageSymlinks(pkgName: string) { - // special case: dashboard needs monaco-editor to be symlink into the local node_modules directory. - if (pkgName === dashboardPackage) { +function getPackageSymlinks(version: string, pkgName: string) { + // special case: dashboard of version 0.1 needs monaco-editor to be symlink into the local node_modules directory. + // with 0.2 and onwards monaco-editor is built with webpack and included in the build output. + if (pkgName === dashboardPackage && version === "0.1") { return ["monaco-editor"]; } diff --git a/src/nodecgIOVersions.ts b/src/nodecgIOVersions.ts index 994e32e..38d3812 100644 --- a/src/nodecgIOVersions.ts +++ b/src/nodecgIOVersions.ts @@ -47,10 +47,27 @@ const version01Services = { youtube: "YoutubeServiceClient", }; -export const supportedNodeCGIORange = new semver.Range("<=0.1"); +const version02Services = { + ...version01Services, + artnet: "ArtNetServiceClient", + atem: "AtemServiceClient", + dbus: "DBusClient", + debug: "DebugHelper", + "discord-rpc": "DiscordRpcClient", + "elgato-light": "ElgatoLightClient", + googleapis: "GoogleApisServiceClient", + github: "GitHubClient", + "mqtt-client": "MQTTClientServiceClient", + shlink: "ShlinkServiceClient", + sql: "SQLClient", + youtube: undefined, +}; + +export const supportedNodeCGIORange = new semver.Range("<=0.2"); -export const versionServiceMap: Record> = { +export const versionServiceMap: Record> = { "0.1": version01Services, + "0.2": version02Services, }; /** @@ -65,7 +82,12 @@ export function getServicesForVersion(version: string): string[] { throw new Error(`Don't have any service list for version ${version}. Something might be wrong here.`); } - return Object.keys(services); + const serviceNames = Object.keys(services) + // only services which have a corresponding service class + // this is not the case when e.g. the service only existed in a previous version + .filter((svcName) => services[svcName] !== undefined); + serviceNames.sort(); + return serviceNames; } /** @@ -76,6 +98,10 @@ export function getServicesForVersion(version: string): string[] { */ export function getServiceClientName(service: string, version: string): string { const svcClientMapping = versionServiceMap[version]; - const clientName = svcClientMapping[service]; + const clientName = svcClientMapping?.[service]; + if (clientName === undefined) { + throw new Error(`Access of undefined service client name for ${service}@${version}`); + } + return clientName; } diff --git a/src/utils/nodecgInstallation.ts b/src/utils/nodecgInstallation.ts index 3c93848..f117b0b 100644 --- a/src/utils/nodecgInstallation.ts +++ b/src/utils/nodecgInstallation.ts @@ -47,7 +47,12 @@ async function isNodeCGDirectory(dir: string): Promise { */ export async function getNodeCGVersion(nodecgDir: string): Promise { const packageJson = await readPackageJson(nodecgDir); - return new SemVer(packageJson["version"]); + const version = packageJson["version"]; + if (version === undefined) { + throw new Error("Version field is missin in the NodeCG package.json."); + } + + return new SemVer(version); } async function readPackageJson(nodecgDir: string): Promise> { diff --git a/test/install/development.ts b/test/install/development.ts index a0037b6..fca17dd 100644 --- a/test/install/development.ts +++ b/test/install/development.ts @@ -93,7 +93,7 @@ describe("getGitRepo", () => { test("should use correct git url for nodecg-io and docs", async () => { await dev.getGitRepo(nodecgIODir, "nodecg-io"); await dev.getGitRepo(nodecgIODir, "nodecg-io-docs"); - expect(fetchSpy.mock.calls[0][0].url?.endsWith("nodecg-io.git")).toBe(true); - expect(fetchSpy.mock.calls[1][0].url?.endsWith("nodecg-io-docs.git")).toBe(true); + expect(fetchSpy.mock.calls[0]?.[0].url?.endsWith("nodecg-io.git")).toBe(true); + expect(fetchSpy.mock.calls[1]?.[0].url?.endsWith("nodecg-io-docs.git")).toBe(true); }); }); diff --git a/test/install/prompt.ts b/test/install/prompt.ts index af2a131..0315633 100644 --- a/test/install/prompt.ts +++ b/test/install/prompt.ts @@ -20,8 +20,8 @@ describe("getCompatibleVersions", () => { const spy = jest.spyOn(logger, "warn"); await getCompatibleVersions(compatibleRange); expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).toContain("Cannot install"); - expect(spy.mock.calls[0][0]).toContain("1.0, 1.1"); + expect(spy.mock.calls[0]?.[0]).toContain("Cannot install"); + expect(spy.mock.calls[0]?.[0]).toContain("1.0, 1.1"); }); }); diff --git a/test/uninstall/index.ts b/test/uninstall/index.ts index 900d00a..6ed2e0b 100644 --- a/test/uninstall/index.ts +++ b/test/uninstall/index.ts @@ -35,13 +35,13 @@ describe("uninstall", () => { expect(spyManageBundleDir).toBeCalledTimes(3); // Should remove nodecg-io directory and sample bundle directory (if applicable) - expect(spyManageBundleDir.mock.calls[0][1]).toBe(nodecgIODir); - expect(spyManageBundleDir.mock.calls[1][1]).toBe(path.join(nodecgIODir, "services")); - expect(spyManageBundleDir.mock.calls[2][1]).toBe(path.join(nodecgIODir, "samples")); + expect(spyManageBundleDir.mock.calls[0]?.[1]).toBe(nodecgIODir); + expect(spyManageBundleDir.mock.calls[1]?.[1]).toBe(path.join(nodecgIODir, "services")); + expect(spyManageBundleDir.mock.calls[2]?.[1]).toBe(path.join(nodecgIODir, "samples")); // Should remove them, not add them - expect(spyManageBundleDir.mock.calls[0][2]).toBe(false); - expect(spyManageBundleDir.mock.calls[1][2]).toBe(false); - expect(spyManageBundleDir.mock.calls[2][2]).toBe(false); + expect(spyManageBundleDir.mock.calls[0]?.[2]).toBe(false); + expect(spyManageBundleDir.mock.calls[1]?.[2]).toBe(false); + expect(spyManageBundleDir.mock.calls[2]?.[2]).toBe(false); }); test("should remove nodecg-io directory", async () => { diff --git a/test/utils/fs.ts b/test/utils/fs.ts index 1d606fd..36c558e 100644 --- a/test/utils/fs.ts +++ b/test/utils/fs.ts @@ -74,13 +74,13 @@ describe("executeCommand", () => { test("should inherit io streams", async () => { const spy = jest.spyOn(child_process, "spawn"); await executeCommand("exit", ["0"]); - expect(spy.mock.calls[0][2].stdio).toBe("inherit"); + expect(spy.mock.calls[0]?.[2].stdio).toBe("inherit"); }); test("should log the command that gets executed", async () => { const spy = jest.spyOn(logger, "info"); await executeCommand("exit", ["0"]); expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).toContain("exit 0"); + expect(spy.mock.calls[0]?.[0]).toContain("exit 0"); }); }); diff --git a/test/utils/npm/npmVersion.ts b/test/utils/npm/npmVersion.ts index 9322e55..902478f 100644 --- a/test/utils/npm/npmVersion.ts +++ b/test/utils/npm/npmVersion.ts @@ -41,7 +41,7 @@ describe("getNpmVersion", () => { const execSpy = createExecMock(validNpmVersion); await getNpmVersion(); expect(execSpy).toHaveBeenCalled(); - expect(execSpy.mock.calls[0][0]).toBe("npm --version"); + expect(execSpy.mock.calls[0]?.[0]).toBe("npm --version"); }); test("should return undefined if npm is not installed", async () => { diff --git a/test/utils/npm/pkgManagement.ts b/test/utils/npm/pkgManagement.ts index 3df1baf..27e3829 100644 --- a/test/utils/npm/pkgManagement.ts +++ b/test/utils/npm/pkgManagement.ts @@ -22,18 +22,18 @@ describe("runNpmInstall", () => { test("should execute npm install --prod when installing prod only", async () => { await runNpmInstall(fsRoot, true); expect(execMock).toHaveBeenCalled(); - expect(execMock.mock.calls[0][0]).toBe(npmCommand); - expect(execMock.mock.calls[0][1][0]).toBe("install"); - expect(execMock.mock.calls[0][1][1]).toBe("--prod"); - expect(execMock.mock.calls[0][2]).toBe(fsRoot); + expect(execMock.mock.calls[0]?.[0]).toBe(npmCommand); + expect(execMock.mock.calls[0]?.[1]?.[0]).toBe("install"); + expect(execMock.mock.calls[0]?.[1]?.[1]).toBe("--prod"); + expect(execMock.mock.calls[0]?.[2]).toBe(fsRoot); }); test("should execute npm install when installing prod and dev", async () => { await runNpmInstall(fsRoot, false); expect(execMock).toHaveBeenCalled(); - expect(execMock.mock.calls[0][0]).toBe(npmCommand); - expect(execMock.mock.calls[0][1][0]).toBe("install"); - expect(execMock.mock.calls[0][1].length).toBe(1); + expect(execMock.mock.calls[0]?.[0]).toBe(npmCommand); + expect(execMock.mock.calls[0]?.[1]?.[0]).toBe("install"); + expect(execMock.mock.calls[0]?.[1].length).toBe(1); }); }); @@ -41,9 +41,9 @@ describe("runNpmBuild", () => { test("should execute install script with passed arguments", async () => { await runNpmBuild(fsRoot, "arg"); expect(execMock).toHaveBeenCalled(); - expect(execMock.mock.calls[0][0]).toBe(npmCommand); - expect(execMock.mock.calls[0][1][1]).toBe("build"); - expect(execMock.mock.calls[0][1][2]).toBe("arg"); // Custom arg from above + expect(execMock.mock.calls[0]?.[0]).toBe(npmCommand); + expect(execMock.mock.calls[0]?.[1]?.[1]).toBe("build"); + expect(execMock.mock.calls[0]?.[1]?.[2]).toBe("arg"); // Custom arg from above }); }); @@ -61,10 +61,10 @@ describe("createNpmSymlinks", () => { expect(spy).toHaveBeenCalled(); // should create it in /nodecg-io-core/node_modules/test-abc (local node_modules) - expect(spy.mock.calls[0][1]).toBe(path.join(fsRoot, corePkg.path, "node_modules", "test-abc")); + expect(spy.mock.calls[0]?.[1]).toBe(path.join(fsRoot, corePkg.path, "node_modules", "test-abc")); // should point to /node_modules/test-abc (hoisted package) - expect(spy.mock.calls[0][0]).toBe(path.join(fsRoot, "node_modules", "test-abc")); + expect(spy.mock.calls[0]?.[0]).toBe(path.join(fsRoot, "node_modules", "test-abc")); }); }); diff --git a/tsconfig.json b/tsconfig.json index 1e1837f..847ccb2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "noImplicitThis": true, "strictNullChecks": true, "skipLibCheck": true, + "noUncheckedIndexedAccess": true, "module": "CommonJS", "outDir": "build"