Skip to content

Commit

Permalink
feat: add support for nodecg-io 0.2 (#39)
Browse files Browse the repository at this point in the history
* feat: add support for nodecg-io 0.2

* Fix test compile errors due to unchecked array access
  • Loading branch information
hlxid authored Nov 19, 2021
1 parent 0fac3ea commit 7797f83
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 3 additions & 2 deletions src/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ async function install(): Promise<void> {
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}...`);
Expand All @@ -56,7 +57,7 @@ async function install(): Promise<void> {

// 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"),
Expand Down
11 changes: 6 additions & 5 deletions src/install/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -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"];
}

Expand Down
34 changes: 30 additions & 4 deletions src/nodecgIOVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, string>> = {
export const versionServiceMap: Record<string, Record<string, string | undefined>> = {
"0.1": version01Services,
"0.2": version02Services,
};

/**
Expand All @@ -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;
}

/**
Expand All @@ -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;
}
7 changes: 6 additions & 1 deletion src/utils/nodecgInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ async function isNodeCGDirectory(dir: string): Promise<boolean> {
*/
export async function getNodeCGVersion(nodecgDir: string): Promise<SemVer> {
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<Record<string, string>> {
Expand Down
4 changes: 2 additions & 2 deletions test/install/development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 2 additions & 2 deletions test/install/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
12 changes: 6 additions & 6 deletions test/uninstall/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions test/utils/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
2 changes: 1 addition & 1 deletion test/utils/npm/npmVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
24 changes: 12 additions & 12 deletions test/utils/npm/pkgManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@ 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);
});
});

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
});
});

Expand All @@ -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"));
});
});

Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"noImplicitThis": true,
"strictNullChecks": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,

"module": "CommonJS",
"outDir": "build"
Expand Down

0 comments on commit 7797f83

Please sign in to comment.