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

Commit 965518d

Browse files
committed
Start with bundle generation. Can already gen a package.json
1 parent c7d8718 commit 965518d

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed

src/generate/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { CommandModule } from "yargs";
2+
import * as chalk from "chalk";
3+
import * as path from "path";
4+
import * as fs from "fs";
5+
import { logger } from "../log";
6+
import { directoryExists, findNodeCGDirectory, getNodeCGIODirectory } from "../fsUtils";
7+
import { ProductionInstallation, readInstallInfo } from "../installation";
8+
import { corePackages } from "../install/nodecgIOVersions";
9+
import { GenerationOptions, promptGenerationOpts } from "./prompt";
10+
import { manageBundleDir } from "../nodecgConfig";
11+
12+
export const yellowInstallCommand = chalk.yellow("nodecg-io install");
13+
const yellowGenerateCommand = chalk.yellow("nodecg-io generate");
14+
15+
export const generateModule: CommandModule = {
16+
command: "generate",
17+
// FIXME rework description
18+
describe: "generates bundles that use nodecg-io services",
19+
20+
handler: async () => {
21+
const nodecgDir = await findNodeCGDirectory();
22+
logger.debug(`Detected nodecg installation at ${nodecgDir}.`);
23+
const nodecgIODir = getNodeCGIODirectory(nodecgDir);
24+
const install = await readInstallInfo(nodecgIODir);
25+
if (install === undefined) {
26+
logger.error("nodecg-io is not installed to your local nodecg install.");
27+
logger.error(`Please install it first using this command: ${yellowInstallCommand}`);
28+
process.exit(1);
29+
} else if (install.dev) {
30+
logger.error(`You cannot use ${yellowGenerateCommand} together with a development installation.`);
31+
process.exit(1);
32+
} else if (install.packages.length === corePackages.length) {
33+
// just has core packages without any services installed.
34+
logger.error(`You first need to have at least one service installed to generate a bundle.`);
35+
logger.error(`Please install a service using this command: ${yellowInstallCommand}`);
36+
process.exit(1);
37+
}
38+
39+
const opts = await promptGenerationOpts(nodecgDir, install);
40+
41+
try {
42+
await generateBundle(nodecgDir, opts);
43+
} catch (e) {
44+
logger.error(`Couldn't generate bundle: ${e}`);
45+
process.exit(1);
46+
}
47+
48+
logger.success(`Successfully generated bundle ${opts.bundleName}.`);
49+
},
50+
};
51+
52+
async function generateBundle(nodecgDir: string, opts: GenerationOptions): Promise<void> {
53+
// Create dir if necessary
54+
const bundlePath = path.join(opts.bundleDir, opts.bundleName);
55+
if (!(await directoryExists(bundlePath))) {
56+
await fs.promises.mkdir(bundlePath);
57+
}
58+
59+
const filesInBundleDir = await fs.promises.readdir(bundlePath);
60+
if (filesInBundleDir.length > 0) {
61+
logger.error(`Directory for bundle at ${bundlePath} already exists and contains files.`);
62+
logger.error("Please make sure that you don't have a bundle with the same name already.");
63+
logger.error(
64+
`Also you cannot use this tool to add nodecg-io to a already existing bundle. It only supports generating new ones.`,
65+
);
66+
process.exit(1);
67+
}
68+
69+
await generatePackageJson(bundlePath, opts);
70+
}
71+
72+
async function generatePackageJson(bundlePath: string, opts: GenerationOptions): Promise<void> {
73+
// This shouldn't happen...
74+
if (!opts.servicePackages) throw new Error("servicePackages undefined");
75+
if (!opts.corePackage) throw new Error("corePackage undefined");
76+
77+
const serviceDeps = Object.fromEntries(opts.servicePackages.map((pkg) => [pkg.name, "^" + pkg.version]));
78+
const dependencies = {
79+
"@types/node": "^15.0.1", // TODO: maybe fetch newest version automatically
80+
nodecg: "^1.8.1", // TODO: get actual nodecg version,
81+
typescript: "^4.2.4",
82+
"nodecg-io-core": "^" + opts.corePackage.version,
83+
...serviceDeps,
84+
};
85+
86+
const content = {
87+
name: opts.bundleName,
88+
version: opts.version,
89+
private: true,
90+
nodecg: {
91+
compatibleRange: "^1.4.0",
92+
bundleDependencies: serviceDeps,
93+
},
94+
scripts: {
95+
build: "tsc -b",
96+
watch: "tsc -b -w",
97+
clean: "tsc -b --clean",
98+
},
99+
dependencies,
100+
};
101+
102+
await write(content, bundlePath, "package.json");
103+
}
104+
105+
async function write(content: string | Record<string, unknown>, ...paths: string[]): Promise<void> {
106+
const finalPath = path.join(...paths);
107+
108+
logger.debug(`Writing file at ${finalPath}`);
109+
110+
// Create directory if missing
111+
const parent = path.dirname(finalPath);
112+
if (!(await directoryExists(parent))) {
113+
await fs.promises.mkdir(parent);
114+
}
115+
116+
const str = typeof content === "string" ? content : JSON.stringify(content, null, 4);
117+
await fs.promises.writeFile(finalPath, str);
118+
}

src/generate/prompt.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as semver from "semver";
2+
import * as inquirer from "inquirer";
3+
import * as path from "path";
4+
import { directoryExists } from "../fsUtils";
5+
import { ProductionInstallation } from "../installation";
6+
import { getServicesFromInstall } from "../install/prompt";
7+
import { yellowInstallCommand } from ".";
8+
import { NpmPackage } from "../npm";
9+
import { corePackage } from "../install/nodecgIOVersions";
10+
11+
export interface GenerationOptions {
12+
bundleName: string;
13+
bundleDir: string;
14+
description: string;
15+
version: semver.SemVer;
16+
services: string[];
17+
18+
// These are not from the prompt but generated by using a install info in updateGenOptsPackages
19+
servicePackages?: NpmPackage[];
20+
corePackage?: NpmPackage;
21+
}
22+
23+
const kebabCaseRegex = /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/;
24+
25+
export async function promptGenerationOpts(
26+
nodecgDir: string,
27+
install: ProductionInstallation,
28+
): Promise<GenerationOptions> {
29+
const defaultBundleDir = path.join(nodecgDir, "bundles");
30+
// if we are already in a bundle directory we use the name of the directory as a bundle name and the corresponding bundle dir
31+
const inBundleDir = path.dirname(process.cwd()) === defaultBundleDir;
32+
const bundleName = inBundleDir ? path.basename(process.cwd()) : undefined;
33+
const bundleDir = inBundleDir ? path.dirname(process.cwd()) : defaultBundleDir;
34+
35+
const opts = await inquirer.prompt([
36+
{
37+
type: "input",
38+
name: "bundleName",
39+
message: "How should your new bundle be named?",
40+
default: bundleName,
41+
validate: validateBundleName,
42+
},
43+
{
44+
type: "input",
45+
name: "bundleDir",
46+
message: "In which bundle directory should your bundle be located?",
47+
default: bundleDir,
48+
validate: validateBundleDir,
49+
},
50+
{
51+
type: "input",
52+
name: "description",
53+
message: "Please give a bundle description:",
54+
},
55+
{
56+
type: "input",
57+
name: "version",
58+
message: "With which version number do you want to start?",
59+
default: "0.1.0",
60+
validate: validateVersion,
61+
filter: (ver) => new semver.SemVer(ver),
62+
},
63+
{
64+
type: "checkbox",
65+
name: "services",
66+
message: `Which services would you like to use? (they must be installed through ${yellowInstallCommand})`,
67+
choices: getServicesFromInstall(install, install.version),
68+
validate: validateServiceSelection,
69+
},
70+
// TODO: license
71+
// TODO: dashboard
72+
// TODO: graphic
73+
// TODO: language js/ts
74+
]);
75+
76+
updateGenOptsPackages(opts, install);
77+
return opts;
78+
}
79+
80+
function validateBundleName(str: string): true | string {
81+
if (str.length === 0) {
82+
return "You must provide a bundle name.";
83+
} else if (kebabCaseRegex.exec(str) === null) {
84+
return "Your bundle name should be in kebab case. E.g. my-nodecg-bundle-name.";
85+
} else {
86+
return true;
87+
}
88+
}
89+
90+
async function validateBundleDir(dir: string): Promise<true | string> {
91+
if (await directoryExists(dir)) {
92+
return true;
93+
} else {
94+
return "The directory does not exist.";
95+
}
96+
}
97+
98+
function validateVersion(ver: string): true | string {
99+
try {
100+
new semver.SemVer(ver);
101+
return true;
102+
} catch (_e) {
103+
return "Please enter a valid version number.";
104+
}
105+
}
106+
107+
function validateServiceSelection(services: string[]): true | string {
108+
if (services.length === 0) {
109+
return "You should select at least one service to use.";
110+
} else {
111+
return true;
112+
}
113+
}
114+
115+
function updateGenOptsPackages(opts: GenerationOptions, install: ProductionInstallation) {
116+
opts.corePackage = install.packages.find((pkg) => pkg.name === corePackage);
117+
opts.servicePackages = opts.services.map((svc) => {
118+
const svcPackage = install.packages.find((pkg) => pkg.name.endsWith(svc));
119+
120+
if (svcPackage === undefined) {
121+
throw new Error(`Service ${svc} has no corresponding package in the passed installation.`);
122+
}
123+
return svcPackage;
124+
});
125+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { installModule } from "./install";
33
import { uninstallModule } from "./uninstall";
44
import { version } from "../package.json";
55
import { checkForCliUpdate, ensureNode12 } from "./cli";
6+
import { generateModule } from "./generate";
67

78
// This file gets imported by the index.js file of the repository root.
89

@@ -12,6 +13,7 @@ const argv = yargs(process.argv.slice(2))
1213
.version(version)
1314
.command(installModule)
1415
.command(uninstallModule)
16+
.command(generateModule)
1517
.option("disable-updates", { type: "boolean", description: "Disables check for nodecg-io-cli updates" })
1618
.strict()
1719
.demandCommand().argv;

0 commit comments

Comments
 (0)