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

Add bundle generation #11

Merged
merged 26 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f2b38cb
Start with bundle generation. Can already gen a package.json
hlxid Apr 30, 2021
2c316d4
Generate tsconfig and half broken index.ts files
hlxid Apr 30, 2021
cb2f25a
Reorganize folder structure
hlxid May 1, 2021
f4af22b
Fix generated tsconfig.json
hlxid May 1, 2021
7c9dc66
Lookup service client names from object
hlxid May 1, 2021
b2b3b9f
Fix version in generated package.json
hlxid May 1, 2021
a593c42
Run npm install and build after generating a bundle
hlxid May 1, 2021
dc375c6
Move codegen into separate file
hlxid May 2, 2021
be85a69
Use nodecg version from current install in generated bundles.
hlxid May 2, 2021
501e3b5
Automatically fetch latest version of typescript and @types/node in g…
hlxid May 2, 2021
806a2d9
Add util function to add semver caret to a version
hlxid May 2, 2021
e174402
Only fetch abbreviated npm metadata to save bandwidth and accelerate …
hlxid May 2, 2021
e8f17cc
Return a SemVer everywhere possible in npm utils
hlxid May 2, 2021
8956c47
Support generating bundles in javascript in addition to typescript
hlxid May 2, 2021
5b5795e
Fix incorrect service names
hlxid May 3, 2021
c42f5e3
Add support for generating a very very simple graphic
hlxid May 3, 2021
0d8c5d6
Add support for generating very very simple dashboards
hlxid May 3, 2021
d5a49e8
Refactor GenerationOpts and split it for cleaner computed fields
hlxid May 3, 2021
7bd871e
Enlarge dashboard to width 3 so that the text fits properly
hlxid May 3, 2021
50af981
Generate .gitignore
hlxid May 3, 2021
0dd693b
Remove ts dashboard and license todo
hlxid May 4, 2021
2818e44
Rebuild generate handler to throw exceptions instead of logging and e…
hlxid May 5, 2021
666bb63
Test generate module
hlxid May 6, 2021
7bda40b
Fix generate tests on windows
hlxid May 6, 2021
b2ff747
Refactor package.json and tsconfig.json generation into their own files
hlxid May 6, 2021
b23b563
Add tests for genTsConfig and genGraphic/Dashboard and genExtension
hlxid May 7, 2021
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ node_modules/
nodecg/

# Test coverage output
coverage/
coverage/

# IDEs / Text editors
.idea/
.vscode/
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ module.exports = {
testEnvironment: "node",
// We don't want to test nodecg, and without including it jest fails because it includes a invalid json
modulePathIgnorePatterns: ["/nodecg/"],
testMatch: ["<rootDir>/test/**/**.ts", "!**/testUtils.ts"],
testMatch: ["<rootDir>/test/**/*.ts", "!**/*.util.ts"],
};
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"dependencies": {
"axios": "^0.21.1",
"chalk": "^4.1.0",
"code-block-writer": "^10.1.1",
"find-up": "^5.0.0",
"glob": "^7.1.6",
"gunzip-maybe": "^1.4.2",
Expand Down
120 changes: 120 additions & 0 deletions src/generate/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import CodeBlockWriter from "code-block-writer";
import { getServiceClientName } from "../nodecgIOVersions";
import { ProductionInstallation } from "../utils/installation";
import { CodeLanguage, GenerationOptions } from "./prompt";
import { writeBundleFile } from "./utils";

interface ServiceNames {
name: string;
camelCase: string;
pascalCase: string;
clientName: string;
packageName: string;
}

function kebabCase2CamelCase(str: string): string {
const parts = str.split("-");
const capitalizedParts = parts.map((p, idx) => (idx === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)));
return capitalizedParts.join("");
}

function kebabCase2PascalCase(str: string): string {
const camelCase = kebabCase2CamelCase(str);
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
}

function getServiceNames(serviceBaseName: string, nodecgIOVersion: string): ServiceNames {
return {
name: serviceBaseName,
camelCase: kebabCase2CamelCase(serviceBaseName),
pascalCase: kebabCase2PascalCase(serviceBaseName),
clientName: getServiceClientName(serviceBaseName, nodecgIOVersion),
packageName: `nodecg-io-${serviceBaseName}`,
};
}

export async function genExtension(opts: GenerationOptions, install: ProductionInstallation): Promise<void> {
// Generate further information for each service which is needed to generate the bundle extension.
const services = opts.services.map((svc) => getServiceNames(svc, install.version));

const writer = new CodeBlockWriter();

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

if (opts.language === "typescript") {
genImport(writer, "NodeCG", "nodecg/types/server", opts.language);
// Service import statements
services.forEach((svc) => {
genImport(writer, svc.clientName, svc.packageName, opts.language);
});
}

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

// requireService calls
services.forEach((svc) => genRequireServiceCall(writer, svc, opts.language));

// onAvailable and onUnavailable calls
services.forEach((svc) => {
writer.blankLine();
genOnAvailableCall(writer, svc);

writer.blankLine();
genOnUnavailableCall(writer, svc);
});
});

const fileExtension = opts.language === "typescript" ? "ts" : "js";
await writeBundleFile(writer.toString(), opts.bundlePath, "extension", `index.${fileExtension}`);
}

function genImport(writer: CodeBlockWriter, symbolToImport: string, packageName: string, lang: CodeLanguage) {
if (lang === "typescript") {
writer.write(`import { ${symbolToImport} } from `).quote(packageName).write(";");
} else if (lang === "javascript") {
writer.write(`const ${symbolToImport} = require(`).quote(packageName).write(`).${symbolToImport};`);
} else {
throw new Error("unsupported language: " + lang);
}

writer.write("\n");
}

function genLog(writer: CodeBlockWriter, logMessage: string) {
writer.write("nodecg.log.info(").quote(logMessage).write(");");
}

function genRequireServiceCall(writer: CodeBlockWriter, svc: ServiceNames, lang: CodeLanguage) {
writer.write(`const ${svc.camelCase} = requireService`);

if (lang === "typescript") {
// Add type parameter which is only needed in TypeScript
writer.write(`<${svc.clientName}>`);
}

writer.write(`(nodecg, `).quote(svc.name).write(");");
}

function genOnAvailableCall(writer: CodeBlockWriter, svc: ServiceNames) {
writer
.write(`${svc.camelCase}?.onAvailable(async (${svc.camelCase}Client) => `)
.inlineBlock(() => {
genLog(writer, `${svc.name} service has been updated.`);
writer.writeLine(`// You can now use the ${svc.name} client here.`);
})
.write(");");
}

function genOnUnavailableCall(writer: CodeBlockWriter, svc: ServiceNames) {
writer
.write(`${svc.camelCase}?.onUnavailable(() => `)
.inlineBlock(() => {
genLog(writer, `${svc.name} has been unset.`);
})
.write(");");
}
112 changes: 112 additions & 0 deletions src/generate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { CommandModule } from "yargs";
import * as fs from "fs";
import { logger } from "../utils/log";
import { directoryExists } from "../utils/fs";
import { Installation, ProductionInstallation, readInstallInfo } from "../utils/installation";
import { corePackages } from "../nodecgIOVersions";
import { GenerationOptions, promptGenerationOpts } from "./prompt";
import { runNpmBuild, runNpmInstall } from "../utils/npm";
import { genExtension } from "./extension";
import { findNodeCGDirectory, getNodeCGIODirectory } from "../utils/nodecgInstallation";
import { genDashboard, genGraphic } from "./panel";
import { genTsConfig } from "./tsConfig";
import { writeBundleFile, yellowGenerateCommand, yellowInstallCommand } from "./utils";
import { genPackageJson } from "./packageJson";

export const generateModule: CommandModule = {
command: "generate",
describe: "generates nodecg bundles that use nodecg-io services",

handler: async () => {
try {
const nodecgDir = await findNodeCGDirectory();
logger.debug(`Detected nodecg installation at ${nodecgDir}.`);
const nodecgIODir = getNodeCGIODirectory(nodecgDir);
const install = await readInstallInfo(nodecgIODir);

// Will throw when install is not valid for generating bundles
if (!ensureValidInstallation(install)) return;

const opts = await promptGenerationOpts(nodecgDir, install);

await generateBundle(nodecgDir, opts, install);

logger.success(`Successfully generated bundle ${opts.bundleName}.`);
} catch (e) {
logger.error(`Couldn't generate bundle:\n${e.message ?? e.toString()}`);
process.exit(1);
}
},
};

/**
* Ensures that a installation can be used to generate bundles, meaning nodecg-io is actually installed,
* is not a dev install and has some services installed that can be used.
* Throws an error if the installation cannot be used to generate a bundle with an explanation.
*/
export function ensureValidInstallation(install: Installation | undefined): install is ProductionInstallation {
if (install === undefined) {
throw new Error(
"nodecg-io is not installed to your local nodecg install.\n" +
`Please install it first using this command: ${yellowInstallCommand}`,
);
} else if (install.dev) {
throw new Error(`You cannot use ${yellowGenerateCommand} together with a development installation.`);
} else if (install.packages.length <= corePackages.length) {
// just has core packages without any services installed.
throw new Error(
`You first need to have at least one service installed to generate a bundle.\n` +
`Please install a service using this command: ${yellowInstallCommand}`,
);
}

return true;
}

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

// In case some re-executes the command in a already used bundle name we should not overwrite their stuff and error instead.
const filesInBundleDir = await fs.promises.readdir(opts.bundlePath);
if (filesInBundleDir.length > 0) {
throw new Error(
`Directory for bundle at ${opts.bundlePath} already exists and contains files.\n` +
"Please make sure that you don't have a bundle with the same name already.\n" +
`Also you cannot use this tool to add nodecg-io to a already existing bundle. It only supports generating new ones.`,
);
}

// All of these calls only generate files if they are set accordingly in the GenerationOptions
await genPackageJson(nodecgDir, opts);
await genTsConfig(opts);
await genGitIgnore(opts);
await genExtension(opts, install);
await genGraphic(opts);
await genDashboard(opts);
logger.info("Generated bundle successfully.");

logger.info("Installing dependencies...");
await runNpmInstall(opts.bundlePath, false);

// JavaScript does not to be compiled
if (opts.language === "typescript") {
logger.info("Compiling bundle...");
await runNpmBuild(opts.bundlePath);
}
}

async function genGitIgnore(opts: GenerationOptions): Promise<void> {
// When typescript we want to ignore compilation results.
const languageIgnoredFiles = opts.language === "typescript" ? ["/extension/*.js", "/extension/*.js.map"] : [];
// Usual editors and node_modules directory
const ignoreEntries = ["/node_modules/", "/.vscode/", "/.idea/", ...languageIgnoredFiles];
const content = ignoreEntries.join("\n");
await writeBundleFile(content, opts.bundlePath, ".gitignore");
}
Loading