Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Commit 1cd4d55

Browse files
authored
Merge pull request #11 from codeoverflow-org/feat/generate-bundles
Add bundle generation
2 parents c7d8718 + b23b563 commit 1cd4d55

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1322
-229
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ node_modules/
66
nodecg/
77

88
# Test coverage output
9-
coverage/
9+
coverage/
10+
11+
# IDEs / Text editors
12+
.idea/
13+
.vscode/

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ module.exports = {
33
testEnvironment: "node",
44
// We don't want to test nodecg, and without including it jest fails because it includes a invalid json
55
modulePathIgnorePatterns: ["/nodecg/"],
6-
testMatch: ["<rootDir>/test/**/**.ts", "!**/testUtils.ts"],
6+
testMatch: ["<rootDir>/test/**/*.ts", "!**/*.util.ts"],
77
};

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"dependencies": {
6060
"axios": "^0.21.1",
6161
"chalk": "^4.1.0",
62+
"code-block-writer": "^10.1.1",
6263
"find-up": "^5.0.0",
6364
"glob": "^7.1.6",
6465
"gunzip-maybe": "^1.4.2",

src/generate/extension.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import CodeBlockWriter from "code-block-writer";
2+
import { getServiceClientName } from "../nodecgIOVersions";
3+
import { ProductionInstallation } from "../utils/installation";
4+
import { CodeLanguage, GenerationOptions } from "./prompt";
5+
import { writeBundleFile } from "./utils";
6+
7+
interface ServiceNames {
8+
name: string;
9+
camelCase: string;
10+
pascalCase: string;
11+
clientName: string;
12+
packageName: string;
13+
}
14+
15+
function kebabCase2CamelCase(str: string): string {
16+
const parts = str.split("-");
17+
const capitalizedParts = parts.map((p, idx) => (idx === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)));
18+
return capitalizedParts.join("");
19+
}
20+
21+
function kebabCase2PascalCase(str: string): string {
22+
const camelCase = kebabCase2CamelCase(str);
23+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
24+
}
25+
26+
function getServiceNames(serviceBaseName: string, nodecgIOVersion: string): ServiceNames {
27+
return {
28+
name: serviceBaseName,
29+
camelCase: kebabCase2CamelCase(serviceBaseName),
30+
pascalCase: kebabCase2PascalCase(serviceBaseName),
31+
clientName: getServiceClientName(serviceBaseName, nodecgIOVersion),
32+
packageName: `nodecg-io-${serviceBaseName}`,
33+
};
34+
}
35+
36+
export async function genExtension(opts: GenerationOptions, install: ProductionInstallation): Promise<void> {
37+
// Generate further information for each service which is needed to generate the bundle extension.
38+
const services = opts.services.map((svc) => getServiceNames(svc, install.version));
39+
40+
const writer = new CodeBlockWriter();
41+
42+
genImport(writer, "requireService", opts.corePackage.name, opts.language);
43+
44+
if (opts.language === "typescript") {
45+
genImport(writer, "NodeCG", "nodecg/types/server", opts.language);
46+
// Service import statements
47+
services.forEach((svc) => {
48+
genImport(writer, svc.clientName, svc.packageName, opts.language);
49+
});
50+
}
51+
52+
// global nodecg function
53+
writer.blankLine();
54+
const nodecgVariableType = opts.language === "typescript" ? ": NodeCG" : "";
55+
writer.write(`module.exports = function (nodecg${nodecgVariableType}) `).block(() => {
56+
genLog(writer, `${opts.bundleName} bundle started.`);
57+
writer.blankLine();
58+
59+
// requireService calls
60+
services.forEach((svc) => genRequireServiceCall(writer, svc, opts.language));
61+
62+
// onAvailable and onUnavailable calls
63+
services.forEach((svc) => {
64+
writer.blankLine();
65+
genOnAvailableCall(writer, svc);
66+
67+
writer.blankLine();
68+
genOnUnavailableCall(writer, svc);
69+
});
70+
});
71+
72+
const fileExtension = opts.language === "typescript" ? "ts" : "js";
73+
await writeBundleFile(writer.toString(), opts.bundlePath, "extension", `index.${fileExtension}`);
74+
}
75+
76+
function genImport(writer: CodeBlockWriter, symbolToImport: string, packageName: string, lang: CodeLanguage) {
77+
if (lang === "typescript") {
78+
writer.write(`import { ${symbolToImport} } from `).quote(packageName).write(";");
79+
} else if (lang === "javascript") {
80+
writer.write(`const ${symbolToImport} = require(`).quote(packageName).write(`).${symbolToImport};`);
81+
} else {
82+
throw new Error("unsupported language: " + lang);
83+
}
84+
85+
writer.write("\n");
86+
}
87+
88+
function genLog(writer: CodeBlockWriter, logMessage: string) {
89+
writer.write("nodecg.log.info(").quote(logMessage).write(");");
90+
}
91+
92+
function genRequireServiceCall(writer: CodeBlockWriter, svc: ServiceNames, lang: CodeLanguage) {
93+
writer.write(`const ${svc.camelCase} = requireService`);
94+
95+
if (lang === "typescript") {
96+
// Add type parameter which is only needed in TypeScript
97+
writer.write(`<${svc.clientName}>`);
98+
}
99+
100+
writer.write(`(nodecg, `).quote(svc.name).write(");");
101+
}
102+
103+
function genOnAvailableCall(writer: CodeBlockWriter, svc: ServiceNames) {
104+
writer
105+
.write(`${svc.camelCase}?.onAvailable(async (${svc.camelCase}Client) => `)
106+
.inlineBlock(() => {
107+
genLog(writer, `${svc.name} service has been updated.`);
108+
writer.writeLine(`// You can now use the ${svc.name} client here.`);
109+
})
110+
.write(");");
111+
}
112+
113+
function genOnUnavailableCall(writer: CodeBlockWriter, svc: ServiceNames) {
114+
writer
115+
.write(`${svc.camelCase}?.onUnavailable(() => `)
116+
.inlineBlock(() => {
117+
genLog(writer, `${svc.name} has been unset.`);
118+
})
119+
.write(");");
120+
}

src/generate/index.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { CommandModule } from "yargs";
2+
import * as fs from "fs";
3+
import { logger } from "../utils/log";
4+
import { directoryExists } from "../utils/fs";
5+
import { Installation, ProductionInstallation, readInstallInfo } from "../utils/installation";
6+
import { corePackages } from "../nodecgIOVersions";
7+
import { GenerationOptions, promptGenerationOpts } from "./prompt";
8+
import { runNpmBuild, runNpmInstall } from "../utils/npm";
9+
import { genExtension } from "./extension";
10+
import { findNodeCGDirectory, getNodeCGIODirectory } from "../utils/nodecgInstallation";
11+
import { genDashboard, genGraphic } from "./panel";
12+
import { genTsConfig } from "./tsConfig";
13+
import { writeBundleFile, yellowGenerateCommand, yellowInstallCommand } from "./utils";
14+
import { genPackageJson } from "./packageJson";
15+
16+
export const generateModule: CommandModule = {
17+
command: "generate",
18+
describe: "generates nodecg bundles that use nodecg-io services",
19+
20+
handler: async () => {
21+
try {
22+
const nodecgDir = await findNodeCGDirectory();
23+
logger.debug(`Detected nodecg installation at ${nodecgDir}.`);
24+
const nodecgIODir = getNodeCGIODirectory(nodecgDir);
25+
const install = await readInstallInfo(nodecgIODir);
26+
27+
// Will throw when install is not valid for generating bundles
28+
if (!ensureValidInstallation(install)) return;
29+
30+
const opts = await promptGenerationOpts(nodecgDir, install);
31+
32+
await generateBundle(nodecgDir, opts, install);
33+
34+
logger.success(`Successfully generated bundle ${opts.bundleName}.`);
35+
} catch (e) {
36+
logger.error(`Couldn't generate bundle:\n${e.message ?? e.toString()}`);
37+
process.exit(1);
38+
}
39+
},
40+
};
41+
42+
/**
43+
* Ensures that a installation can be used to generate bundles, meaning nodecg-io is actually installed,
44+
* is not a dev install and has some services installed that can be used.
45+
* Throws an error if the installation cannot be used to generate a bundle with an explanation.
46+
*/
47+
export function ensureValidInstallation(install: Installation | undefined): install is ProductionInstallation {
48+
if (install === undefined) {
49+
throw new Error(
50+
"nodecg-io is not installed to your local nodecg install.\n" +
51+
`Please install it first using this command: ${yellowInstallCommand}`,
52+
);
53+
} else if (install.dev) {
54+
throw new Error(`You cannot use ${yellowGenerateCommand} together with a development installation.`);
55+
} else if (install.packages.length <= corePackages.length) {
56+
// just has core packages without any services installed.
57+
throw new Error(
58+
`You first need to have at least one service installed to generate a bundle.\n` +
59+
`Please install a service using this command: ${yellowInstallCommand}`,
60+
);
61+
}
62+
63+
return true;
64+
}
65+
66+
export async function generateBundle(
67+
nodecgDir: string,
68+
opts: GenerationOptions,
69+
install: ProductionInstallation,
70+
): Promise<void> {
71+
// Create dir if necessary
72+
if (!(await directoryExists(opts.bundlePath))) {
73+
await fs.promises.mkdir(opts.bundlePath);
74+
}
75+
76+
// In case some re-executes the command in a already used bundle name we should not overwrite their stuff and error instead.
77+
const filesInBundleDir = await fs.promises.readdir(opts.bundlePath);
78+
if (filesInBundleDir.length > 0) {
79+
throw new Error(
80+
`Directory for bundle at ${opts.bundlePath} already exists and contains files.\n` +
81+
"Please make sure that you don't have a bundle with the same name already.\n" +
82+
`Also you cannot use this tool to add nodecg-io to a already existing bundle. It only supports generating new ones.`,
83+
);
84+
}
85+
86+
// All of these calls only generate files if they are set accordingly in the GenerationOptions
87+
await genPackageJson(nodecgDir, opts);
88+
await genTsConfig(opts);
89+
await genGitIgnore(opts);
90+
await genExtension(opts, install);
91+
await genGraphic(opts);
92+
await genDashboard(opts);
93+
logger.info("Generated bundle successfully.");
94+
95+
logger.info("Installing dependencies...");
96+
await runNpmInstall(opts.bundlePath, false);
97+
98+
// JavaScript does not to be compiled
99+
if (opts.language === "typescript") {
100+
logger.info("Compiling bundle...");
101+
await runNpmBuild(opts.bundlePath);
102+
}
103+
}
104+
105+
async function genGitIgnore(opts: GenerationOptions): Promise<void> {
106+
// When typescript we want to ignore compilation results.
107+
const languageIgnoredFiles = opts.language === "typescript" ? ["/extension/*.js", "/extension/*.js.map"] : [];
108+
// Usual editors and node_modules directory
109+
const ignoreEntries = ["/node_modules/", "/.vscode/", "/.idea/", ...languageIgnoredFiles];
110+
const content = ignoreEntries.join("\n");
111+
await writeBundleFile(content, opts.bundlePath, ".gitignore");
112+
}

0 commit comments

Comments
 (0)