Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
379 changes: 28 additions & 351 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"devDependencies": {
"@semantic-release/git": "^10.0.0",
"@types/glob": "^7.1.4",
"@types/gunzip-maybe": "^1.4.0",
"@types/inquirer": "^8.1.3",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.3",
Expand All @@ -64,9 +63,7 @@
"axios": "^0.24.0",
"chalk": "^4.1.2",
"code-block-writer": "^11.0.0",
"find-up": "^5.0.0",
"glob": "^7.2.0",
"gunzip-maybe": "^1.4.2",
"inquirer": "^8.2.0",
"isomorphic-git": "^1.10.1",
"semver": "^7.3.5",
Expand Down
2 changes: 1 addition & 1 deletion src/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function genExtension(opts: GenerationOptions, install: ProductionI
genImport(writer, "requireService", opts.corePackage.name, opts.language);

if (opts.language === "typescript") {
genImport(writer, "NodeCG", "nodecg/types/server", opts.language);
genImport(writer, "NodeCG", `${opts.nodeeCGTypingsPackage}/types/server`, opts.language);
// Service import statements
services.forEach((svc) => {
genImport(writer, svc.clientName, svc.packageName, opts.language);
Expand Down
10 changes: 3 additions & 7 deletions src/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const generateModule: CommandModule = {

const opts = await promptGenerationOpts(nodecgDir, install);

await generateBundle(nodecgDir, opts, install);
await generateBundle(opts, install);

logger.success(`Successfully generated bundle ${opts.bundleName}.`);
} catch (e) {
Expand Down Expand Up @@ -63,11 +63,7 @@ export function ensureValidInstallation(install: Installation | undefined): inst
return true;
}

export async function generateBundle(
nodecgDir: string,
opts: GenerationOptions,
install: ProductionInstallation,
): Promise<void> {
export async function generateBundle(opts: GenerationOptions, install: ProductionInstallation): Promise<void> {
// Create dir if necessary
if (!(await directoryExists(opts.bundlePath))) {
await fs.promises.mkdir(opts.bundlePath);
Expand All @@ -84,7 +80,7 @@ export async function generateBundle(
}

// All of these calls only generate files if they are set accordingly in the GenerationOptions
await genPackageJson(nodecgDir, opts);
await genPackageJson(opts);
await genTsConfig(opts);
await genGitIgnore(opts);
await genExtension(opts, install);
Expand Down
17 changes: 8 additions & 9 deletions src/generate/packageJson.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { GenerationOptions } from "./prompt";
import { logger } from "../utils/log";
import { getNodeCGVersion } from "../utils/nodecgInstallation";
import { getLatestPackageVersion } from "../utils/npm";
import { genNodeCGDashboardConfig, genNodeCGGraphicConfig } from "./panel";
import { SemVer } from "semver";
Expand All @@ -17,7 +16,7 @@ type Dependency = [string, string];
* @param nodecgDir the directory in which nodecg is installed
* @param opts the options that the user chose for the bundle.
*/
export async function genPackageJson(nodecgDir: string, opts: GenerationOptions): Promise<void> {
export async function genPackageJson(opts: GenerationOptions): Promise<void> {
const serviceDeps: Dependency[] = opts.servicePackages.map((pkg) => [pkg.name, addSemverCaret(pkg.version)]);

const content = {
Expand All @@ -32,7 +31,7 @@ export async function genPackageJson(nodecgDir: string, opts: GenerationOptions)
},
// These scripts are for compiling TS and thus are only needed when generating a TS bundle
scripts: genScripts(opts),
dependencies: Object.fromEntries(await genDependencies(opts, serviceDeps, nodecgDir)),
dependencies: Object.fromEntries(await genDependencies(opts, serviceDeps)),
};

await writeBundleFile(content, opts.bundlePath, "package.json");
Expand All @@ -46,12 +45,12 @@ export async function genPackageJson(nodecgDir: string, opts: GenerationOptions)
* @param nodecgDir the directory in which nodecg is installed
* @return the dependencies for a bundle with the given options.
*/
async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[], nodecgDir: string) {
async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[]) {
const core = [opts.corePackage.name, addSemverCaret(opts.corePackage.version)];

if (opts.language === "typescript") {
// For typescript we need core, all services (for typings) and special packages like ts itself or node typings.
const deps = [core, ...serviceDeps, ...(await genTypeScriptDependencies(nodecgDir))];
const deps = [core, ...serviceDeps, ...(await genTypeScriptDependencies(opts))];
deps.sort();
return deps;
} else {
Expand All @@ -65,16 +64,16 @@ async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[
* and types for node.
* @param nodecgDir the directory in which nodecg is installed. Used to get nodecg version which will be used by nodecg dependency.
*/
async function genTypeScriptDependencies(nodecgDir: string): Promise<Dependency[]> {
logger.debug("Fetching latest typescript and @types/node versions...");
async function genTypeScriptDependencies(opts: GenerationOptions): Promise<Dependency[]> {
logger.debug(`Fetching latest ${opts.nodeeCGTypingsPackage}, typescript and @types/node versions...`);
const [nodecgVersion, latestNodeTypes, latestTypeScript] = await Promise.all([
getNodeCGVersion(nodecgDir),
getLatestPackageVersion(opts.nodeeCGTypingsPackage),
getLatestPackageVersion("@types/node"),
getLatestPackageVersion("typescript"),
]);

return [
["nodecg", addSemverCaret(nodecgVersion)],
[opts.nodeeCGTypingsPackage, addSemverCaret(nodecgVersion)],
["@types/node", addSemverCaret(latestNodeTypes)],
["typescript", addSemverCaret(latestTypeScript)],
];
Expand Down
2 changes: 2 additions & 0 deletions src/generate/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface PromptedGenerationOptions {
export interface GenerationOptions extends PromptedGenerationOptions {
servicePackages: NpmPackage[];
corePackage: NpmPackage;
nodeeCGTypingsPackage: "nodecg" | "nodecg-types";
bundlePath: string;
}

Expand Down Expand Up @@ -169,5 +170,6 @@ export function computeGenOptsFields(
return svcPackage;
}),
bundlePath: path.join(opts.bundleDir, opts.bundleName),
nodeeCGTypingsPackage: install.version === "0.1" ? "nodecg" : "nodecg-types",
};
}
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;
}
26 changes: 17 additions & 9 deletions src/utils/nodecgInstallation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as findUp from "find-up";
import * as path from "path";
import * as fs from "fs";
import { SemVer } from "semver";
Expand All @@ -7,17 +6,21 @@ import { directoryExists } from "./fs";
/**
* Traverses the filesystem and uses {@link isNodeCGDirectory} to find a local nodecg installation.
*/
export async function findNodeCGDirectory(cwd = process.cwd()): Promise<string> {
const res = await findUp(async (dir) => ((await isNodeCGDirectory(dir)) ? dir : undefined), {
type: "directory",
cwd,
});
if (res === undefined) {
export async function findNodeCGDirectory(dir = process.cwd()): Promise<string> {
if (await isNodeCGDirectory(dir)) {
return dir;
}

const parent = path.dirname(dir);

// We're at the filesystem root because we cannot go one level up more and didn't found a nodecg installation
if (parent === dir) {
throw new Error(
"Couldn't find a nodecg installation. Make sure that you are in the directory of your nodecg installation.",
);
}
return res;

return findNodeCGDirectory(parent);
}

/**
Expand All @@ -44,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 src/utils/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as path from "path";
import { executeCommand, removeDirectory } from "./fs";
import { exec } from "child_process";
import { maxSatisfying, satisfies, SemVer } from "semver";
import gunzip = require("gunzip-maybe");
import * as zlib from "zlib";
import * as tar from "tar-fs";

const npmRegistryEndpoint = "https://registry.npmjs.org/";
Expand Down Expand Up @@ -144,7 +144,7 @@ export async function extractNpmPackageTar(
tarStream: fs.ReadStream,
nodecgIODir: string,
): Promise<void> {
const extractStream = tarStream.pipe(gunzip(3)).pipe(
const extractStream = tarStream.pipe(zlib.createGunzip({ level: 3 })).pipe(
tar.extract(buildNpmPackagePath(pkg, nodecgIODir), {
map: (header) => {
// Content inside the tar is in /package/*, so we need to rewrite the name to not create a directory
Expand Down
Loading