Skip to content

Commit

Permalink
feat(generate): allow generating NodeCG v2 bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
hlxid committed May 19, 2023
1 parent fb00982 commit 8ff3530
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 48 deletions.
22 changes: 16 additions & 6 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
"dockerfile": "Dockerfile"
},

// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/zsh"
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "Orta.vscode-jest"],
"settings": {
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"icon": "terminal-bash"
},
"zsh": {
"path": "zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh"
}
}
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": ["dbaeumer.vscode-eslint"],

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// 9090 is the default nodecg port if you have setup a nodecg install using the install command and want to test it
"forwardPorts": [9090],
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/postCreate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ npm link
# Get nodecg so you can test the cli using this installation
[ ! -d "nodecg" ] && git clone https://github.com/nodecg/nodecg.git

cd nodecg && npm i --omit=dev
cd nodecg && npm i && npm run build
78 changes: 70 additions & 8 deletions src/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ export async function genExtension(opts: GenerationOptions, install: Installatio
const writer = new CodeBlockWriter();

// imports
genImport(writer, "requireService", opts.corePackage.name, opts.language);
genImport(writer, "requireService", opts.corePackage.name, opts.language, false);

if (opts.language === "typescript") {
genImport(writer, "NodeCG", `${opts.nodeCGTypingsPackage}/types/server`, opts.language);
generateNodeCGImport(writer, opts, install);

// Service import statements
services.forEach((svc) => {
genImport(writer, svc.clientName, svc.packageName, opts.language);
genImport(writer, svc.clientName, svc.packageName, opts.language, false);
});
}

// global nodecg function
writer.blankLine();
const nodecgVariableType = opts.language === "typescript" ? ": NodeCG" : "";
writer.write(`module.exports = function (nodecg${nodecgVariableType}) `).block(() => {
writer.write(`module.exports = function (nodecg${getNodeCGType(opts, install)}) `).block(() => {
genLog(writer, `${opts.bundleName} bundle started.`);
writer.blankLine();

Expand All @@ -76,18 +76,68 @@ export async function genExtension(opts: GenerationOptions, install: Installatio
await writeBundleFile(writer.toString(), opts.bundlePath, "extension", `index.${fileExtension}`);
}

function genImport(writer: CodeBlockWriter, symbolToImport: string, packageName: string, lang: CodeLanguage) {
function genImport(
writer: CodeBlockWriter,
symbolToImport: string,
packageName: string,
lang: CodeLanguage,
isDefaultImport: boolean,
) {
if (lang === "typescript") {
writer.write(`import { ${symbolToImport} } from `).quote(packageName).write(";");
writer.write("import ");

if (!isDefaultImport) {
writer.write("{ ");
}
writer.write(symbolToImport);
if (!isDefaultImport) {
writer.write(" }");
}

writer.write(` from `).quote(packageName).write(";");
} else if (lang === "javascript") {
writer.write(`const ${symbolToImport} = require(`).quote(packageName).write(`).${symbolToImport};`);
writer.write(`const ${symbolToImport} = require(`).quote(packageName).write(")");

if (!isDefaultImport) {
writer.write(`.${symbolToImport}`);
}

writer.write(";");
} else {
throw new Error("unsupported language: " + lang);
}

writer.write("\n");
}

export function determineNodeCGImportPath(opts: GenerationOptions, install: Installation): string {
if (install.version === "0.1") {
// nodecg-io 0.1 is only compatible with the NodeCG typings bundled inside the full nodecg package
return "nodecg/types/server";
} else if (install.version === "0.2" || opts.nodeCGVersion.major === 1) {
// nodecg-io 0.2 is only compatible with nodecg-types.
// Newer versions are compatible with both: nodecg-types (NodeCG v1) and @nodecg/types (NodeCG v2)
// There we check the current nodecg version to determine which import to use.
return "nodecg-types/types/server";
} else if (opts.nodeCGVersion.major === 2) {
// All versions from 0.3 and upwards support the official @nodecg/types package for NodeCG v2
return "@nodecg/types";
} else {
throw new Error(
"unable to determine nodecg typings import for nodecg " +
opts.nodeCGVersion +
" and nodecg-io " +
install.version,
);
}
}

function generateNodeCGImport(writer: CodeBlockWriter, opts: GenerationOptions, install: Installation) {
const importPath = determineNodeCGImportPath(opts, install);
const isDefaultImport = opts.nodeCGVersion.major === 2 && install.version !== "0.1" && install.version !== "0.2";
genImport(writer, "NodeCG", importPath, opts.language, isDefaultImport);
}

function genLog(writer: CodeBlockWriter, logMessage: string) {
writer.write("nodecg.log.info(").quote(logMessage).write(");");
}
Expand Down Expand Up @@ -121,3 +171,15 @@ function genOnUnavailableCall(writer: CodeBlockWriter, svc: ServiceNames) {
})
.write(");");
}

function getNodeCGType(opts: GenerationOptions, install: Installation): string {
if (opts.language !== "typescript") {
return "";
}

if (install.version === "0.1" || install.version === "0.2" || opts.nodeCGVersion.major === 1) {
return ": NodeCG";
} else {
return ": NodeCG.ServerAPI";
}
}
27 changes: 18 additions & 9 deletions src/generate/packageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { genNodeCGDashboardConfig, genNodeCGGraphicConfig } from "./panel";
import { SemVer } from "semver";
import { writeBundleFile } from "./utils";
import { Installation } from "../utils/installation";
import { determineNodeCGImportPath } from "./extension";

// Loaction where the development tarballs are hosted.
export const developmentPublishRootUrl = "https://codeoverflow-org.github.io/nodecg-io-publish/";
Expand Down Expand Up @@ -54,7 +55,7 @@ async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[

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(opts))];
const deps = [core, ...serviceDeps, ...(await genTypeScriptDependencies(opts, install))];
deps.sort();
return deps;
} else {
Expand All @@ -66,22 +67,30 @@ async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[
/**
* Generates all extra dependencies that are needed when having a bundle in TS. Meaning typescript itself, nodecg for typings
* 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.
* @return the dependencies that are needed for a TS bundle.
*/
async function genTypeScriptDependencies(opts: GenerationOptions): Promise<Dependency[]> {
logger.debug(
`Fetching latest ${opts.nodeCGTypingsPackage}, nodecg-io-tsconfig, typescript and @types/node versions...`,
);
const [nodecgVersion, latestTsConfig, latestTypeScript, latestNodeTypes] = await Promise.all([
getLatestPackageVersion(opts.nodeCGTypingsPackage),
async function genTypeScriptDependencies(opts: GenerationOptions, install: Installation): Promise<Dependency[]> {
const nodecgTypingPackage = determineNodeCGImportPath(opts, install).replace("/types/server", "");
if (!nodecgTypingPackage) {
throw new Error("Could not determine nodecg typing package");
}

let nodecgTypingVersion = opts.nodeCGVersion;
if (nodecgTypingPackage === "nodecg-types") {
logger.debug(`Fetching latest nodecg-types version...`);
nodecgTypingVersion = await getLatestPackageVersion("nodecg-types");
}

logger.debug(`Fetching latest nodecg-io-tsconfig, typescript and @types/node versions...`);
const [latestTsConfig, latestTypeScript, latestNodeTypes] = await Promise.all([
getLatestPackageVersion("nodecg-io-tsconfig"),
getLatestPackageVersion("typescript"),
getLatestPackageVersion("@types/node"),
]);

return [
["@types/node", `^${latestNodeTypes}`],
[opts.nodeCGTypingsPackage, `^${nodecgVersion}`],
[nodecgTypingPackage, `^${nodecgTypingVersion}`],
["nodecg-io-tsconfig", `^${latestTsConfig}`],
["typescript", `^${latestTypeScript}`],
];
Expand Down
14 changes: 7 additions & 7 deletions src/generate/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getServicesFromInstall } from "../install/prompt";
import { yellowInstallCommand } from "./utils";
import { findNpmPackages, NpmPackage } from "../utils/npm";
import { corePackage } from "../nodecgIOVersions";
import { getNodeCGIODirectory } from "../utils/nodecgInstallation";
import { getNodeCGIODirectory, getNodeCGVersion } from "../utils/nodecgInstallation";

/**
* Describes all options for bundle generation a user has answered with inside the inquirer prompt
Expand All @@ -29,7 +29,7 @@ export interface PromptedGenerationOptions {
export interface GenerationOptions extends PromptedGenerationOptions {
servicePackages: NpmPackage[];
corePackage: NpmPackage;
nodeCGTypingsPackage: "nodecg" | "nodecg-types";
nodeCGVersion: semver.SemVer;
bundlePath: string;
}

Expand Down Expand Up @@ -106,7 +106,7 @@ export async function promptGenerationOpts(nodecgDir: string, install: Installat
},
]);

return computeGenOptsFields(opts, install, installedPackages);
return await computeGenOptsFields(opts, installedPackages, nodecgDir);
}

// region prompt validation
Expand Down Expand Up @@ -156,11 +156,11 @@ function validateServiceSelection(services: string[]): true | string {
* @param install the current nodecg-io installation. Used to get installed packages/exact versions.
* @return opts including computed fields.
*/
export function computeGenOptsFields(
export async function computeGenOptsFields(
opts: PromptedGenerationOptions,
install: Installation,
installedPackages: NpmPackage[],
): GenerationOptions {
nodecgDir: string,
): Promise<GenerationOptions> {
const corePkg = installedPackages.find((pkg) => pkg.name === corePackage);
if (corePkg === undefined) {
throw new Error("Core package in installation info could not be found.");
Expand All @@ -178,6 +178,6 @@ export function computeGenOptsFields(
return svcPackage;
}),
bundlePath: path.join(opts.bundleDir, opts.bundleName),
nodeCGTypingsPackage: install.version === "0.1" ? "nodecg" : "nodecg-types",
nodeCGVersion: await getNodeCGVersion(nodecgDir),
};
}
48 changes: 34 additions & 14 deletions test/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as installation from "../../src/utils/installation";
import * as fsUtils from "../../src/utils/fs";
import * as npm from "../../src/utils/npm";
import { ensureValidInstallation, generateBundle } from "../../src/generate";
import { computeGenOptsFields, GenerationOptions } from "../../src/generate/prompt";
import { defaultOpts, defaultOptsPrompt, jsOpts } from "./opts.util";
import { GenerationOptions } from "../../src/generate/prompt";
import { defaultOpts, jsOpts } from "./opts.util";
import { developmentPublishRootUrl } from "../../src/generate/packageJson";
import { Installation } from "../../src/utils/installation";

Expand Down Expand Up @@ -75,10 +75,10 @@ describe("generateBundle", () => {

describe("genPackageJson", () => {
// We don't have a good type for a package.json and this is only testing code so this should be fine.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function genPackageJSON(
opts: GenerationOptions = defaultOpts,
install: Installation = validProdInstall,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> {
await generateBundle(opts, install);
const packageJsonStr = await vol.promises.readFile(packageJsonPath);
Expand Down Expand Up @@ -108,9 +108,10 @@ describe("genPackageJson", () => {
expect(e).toEqual(expect.arrayContaining([[twitchChatPkg.name, `^${twitchChatPkg.version}`]]));

// These dependencies should always have the latest version which is fetched by the mocked getLatestPackageVersion
// And nodecg should be the version of the NodeCG install
expect(e).toEqual(expect.arrayContaining([["typescript", `^1.2.3`]]));
expect(e).toEqual(expect.arrayContaining([["@types/node", `^1.2.3`]]));
expect(e).toEqual(expect.arrayContaining([["nodecg", `^1.2.3`]]));
expect(e).toEqual(expect.arrayContaining([["nodecg", `^${defaultOpts.nodeCGVersion}`]]));
});

test("should get dependencies using tarballs if a development install is used", async () => {
Expand All @@ -119,18 +120,37 @@ describe("genPackageJson", () => {
expect(deps["nodecg-io-core"]).toContain(developmentPublishRootUrl);
});

test("should use nodecg-types for 0.2 or higher", async () => {
const opts = computeGenOptsFields(
defaultOptsPrompt,
{ ...validProdInstall, version: "0.2" },
validProdInstall.packages,
);
const deps = (await genPackageJSON(opts))["dependencies"];
const e = Object.entries(deps);
expect(e).toEqual(expect.arrayContaining([[twitchChatPkg.name, `^${twitchChatPkg.version}`]]));
test("should use nodecg-types for 0.2", async () => {
const install = {
...validProdInstall,
version: "0.2",
};
const deps = (await genPackageJSON(defaultOpts, install))["dependencies"];

// These dependencies should always have the latest version which is fetched by the mocked getLatestPackageVersion
expect(e).toEqual(expect.arrayContaining([["nodecg-types", `^1.2.3`]]));
expect(deps["nodecg-types"]).toBe("^1.2.3");
});

// TODO: seperate in 0.3 and dev once 0.3 has been released and added
test("should use nodecg-types for 0.3 or higher/dev if NodeCG v1", async () => {
const opts = {
...defaultOpts,
nodeCGVersion: new SemVer("1.9.0"),
};
const deps = (await genPackageJSON(opts, validDevInstall))["dependencies"];

// These dependencies should always have the latest version which is fetched by the mocked getLatestPackageVersion
expect(deps["nodecg-types"]).toBe("^1.2.3");
});

test("should use @nodecg/types for 0.3 or higher/dev if NodeCG v2", async () => {
const opts = {
...defaultOpts,
nodeCGVersion: new SemVer("2.1.0"),
};
const deps = (await genPackageJSON(opts, validDevInstall))["dependencies"];
expect(deps["@nodecg/types"]).toBeDefined();
expect(deps["@nodecg/types"]).toBe("^2.1.0"); // Should be same version as NodeCG
});

test("should have build scripts if typescript", async () => {
Expand Down
12 changes: 9 additions & 3 deletions test/generate/opts.util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { computeGenOptsFields, GenerationOptions, PromptedGenerationOptions } from "../../src/generate/prompt";
import { GenerationOptions, PromptedGenerationOptions } from "../../src/generate/prompt";
import * as path from "path";
import { fsRoot, twitchChatPkg, validProdInstall } from "../test.util";
import { corePkg, fsRoot, twitchChatPkg } from "../test.util";
import { SemVer } from "semver";

export const defaultOptsPrompt: PromptedGenerationOptions = {
Expand All @@ -14,5 +14,11 @@ export const defaultOptsPrompt: PromptedGenerationOptions = {
dashboard: false,
};

export const defaultOpts = computeGenOptsFields(defaultOptsPrompt, validProdInstall, validProdInstall.packages);
export const defaultOpts: GenerationOptions = {
...defaultOptsPrompt,
servicePackages: [twitchChatPkg],
corePackage: corePkg,
nodeCGVersion: new SemVer("2.0.0"),
bundlePath: path.join(fsRoot, "bundles", "test-bundle"),
};
export const jsOpts: GenerationOptions = { ...defaultOpts, language: "javascript" };

0 comments on commit 8ff3530

Please sign in to comment.