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

Release cli v0.3.0 #63

Merged
merged 2 commits into from
Dec 17, 2021
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ Uses your installed nodecg-io version and services, meaning you need to have the
These generated bundles are only meant as a starting point, you may probably do more things like creating a git repository for your bundle,
add a licence, or add other tools like linters.

Also, this command currently only works with installs of released versions and not with development installs. This is because all bundles using nodecg-io depend on `nodecg-io-core` and if you use typescript each used service as well. For development installs these are not published on npm, and you would need some way of linking the packages locally.
If you are using a released version of nodecg-io (aka. a production install) the nodecg-io packages get fetched directly from npm.
If you are using a development version of nodecg-io these get fetched as tarballs from the [nodecg-io-publish repository](https://github.com/codeoverflow-org/nodecg-io-publish).

## A note about versioning

Expand All @@ -59,9 +60,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.
| `0.3`, `0.2` | `0.2`, `0.1` |

## Developer workflow

Expand Down
11 changes: 7 additions & 4 deletions src/generate/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CodeBlockWriter from "code-block-writer";
import { getServiceClientName } from "../nodecgIOVersions";
import { ProductionInstallation } from "../utils/installation";
import { Installation } from "../utils/installation";
import { CodeLanguage, GenerationOptions } from "./prompt";
import { writeBundleFile } from "./utils";

Expand Down Expand Up @@ -33,12 +33,15 @@ function getServiceNames(serviceBaseName: string, nodecgIOVersion: string): Serv
};
}

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));
export async function genExtension(opts: GenerationOptions, install: Installation): Promise<void> {
// Generate all variants of the service names if were doing it from a production install.
// We can't generate the imports and stuff if we currently have a development install because
// the service names for each version are hardcoded and unknown for a development version.
const services = install.dev === false ? opts.services.map((svc) => getServiceNames(svc, install.version)) : [];

const writer = new CodeBlockWriter();

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

if (opts.language === "typescript") {
Expand Down
18 changes: 8 additions & 10 deletions src/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ 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 { Installation, 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 { writeBundleFile, yellowInstallCommand } from "./utils";
import { genPackageJson } from "./packageJson";

export const generateModule: CommandModule = {
Expand Down Expand Up @@ -40,19 +40,17 @@ export const generateModule: CommandModule = {
};

/**
* 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.
* Ensures that a installation can be used to generate bundles, meaning nodecg-io is actually installed
* including at least one service that can be used for generating a bundle.
* 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 {
export function ensureValidInstallation(install: Installation | undefined): install is Installation {
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) {
} else if (install.dev === false && 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` +
Expand All @@ -63,7 +61,7 @@ export function ensureValidInstallation(install: Installation | undefined): inst
return true;
}

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

// All of these calls only generate files if they are set accordingly in the GenerationOptions
await genPackageJson(opts);
await genPackageJson(opts, install);
await genTsConfig(opts);
await genGitIgnore(opts);
await genExtension(opts, install);
Expand Down
42 changes: 25 additions & 17 deletions src/generate/packageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { getLatestPackageVersion } from "../utils/npm";
import { genNodeCGDashboardConfig, genNodeCGGraphicConfig } from "./panel";
import { SemVer } from "semver";
import { writeBundleFile } from "./utils";
import { Installation } from "../utils/installation";

// Loaction where the development tarballs are hosted.
export const developmentPublishRootUrl = "https://codeoverflow-org.github.io/nodecg-io-publish/";

/**
* A dependency on a npm package. First field is the package name and the second field is the version.
Expand All @@ -13,25 +17,25 @@ type Dependency = [string, string];
/**
* Generates the whole package.json file for the bundle.
*
* @param nodecgDir the directory in which nodecg is installed
* @param opts the options that the user chose for the bundle.
* @param install the nodecg-io installation that will used to get the versions of the various packages.
*/
export async function genPackageJson(opts: GenerationOptions): Promise<void> {
const serviceDeps: Dependency[] = opts.servicePackages.map((pkg) => [pkg.name, addSemverCaret(pkg.version)]);
export async function genPackageJson(opts: GenerationOptions, install: Installation): Promise<void> {
const serviceDeps = opts.servicePackages.map((pkg) => getNodecgIODependency(pkg.name, pkg.version, install));

const content = {
name: opts.bundleName,
version: opts.version.version,
private: true,
nodecg: {
compatibleRange: addSemverCaret("1.4.0"),
bundleDependencies: Object.fromEntries(serviceDeps),
compatibleRange: "^1.4.0",
bundleDependencies: Object.fromEntries(opts.servicePackages.map((pkg) => [pkg.name, `^${pkg.version}`])),
graphics: genNodeCGGraphicConfig(opts),
dashboardPanels: genNodeCGDashboardConfig(opts),
},
// 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)),
dependencies: Object.fromEntries(await genDependencies(opts, serviceDeps, install)),
};

await writeBundleFile(content, opts.bundlePath, "package.json");
Expand All @@ -45,8 +49,8 @@ export async function genPackageJson(opts: GenerationOptions): Promise<void> {
* @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[]) {
const core = [opts.corePackage.name, addSemverCaret(opts.corePackage.version)];
async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[], install: Installation) {
const core = getNodecgIODependency(opts.corePackage.name, opts.corePackage.version, install);

if (opts.language === "typescript") {
// For typescript we need core, all services (for typings) and special packages like ts itself or node typings.
Expand All @@ -73,9 +77,9 @@ async function genTypeScriptDependencies(opts: GenerationOptions): Promise<Depen
]);

return [
[opts.nodeeCGTypingsPackage, addSemverCaret(nodecgVersion)],
["@types/node", addSemverCaret(latestNodeTypes)],
["typescript", addSemverCaret(latestTypeScript)],
[opts.nodeeCGTypingsPackage, `^${nodecgVersion}`],
["@types/node", `^${latestNodeTypes}`],
["typescript", `^${latestTypeScript}`],
];
}

Expand All @@ -86,6 +90,7 @@ async function genTypeScriptDependencies(opts: GenerationOptions): Promise<Depen
*/
function genScripts(opts: GenerationOptions) {
if (opts.language !== "typescript") {
// For JS we don't need any scripts to build anythiing.
return undefined;
}

Expand All @@ -98,11 +103,14 @@ function genScripts(opts: GenerationOptions) {
}

/**
* Adds the semver caret operator to a given version to allow or minor and patch updates by npm.
*
* @param version the base version
* @return the version with the semver caret operator in front.
* Builds the npm dependency for the package with the passed name and version.
* If this is a production install it will be from the npm registry and
* if it is a development install it will be from a tarball of the nodecg-io-publish repository.
*/
function addSemverCaret(version: string | SemVer): string {
return `^${version}`;
function getNodecgIODependency(packageName: string, version: string | SemVer, install: Installation): Dependency {
if (install.dev) {
return [packageName, `${developmentPublishRootUrl}${packageName}-${version}.tgz`];
} else {
return [packageName, `^${version}`];
}
}
42 changes: 25 additions & 17 deletions src/generate/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import * as semver from "semver";
import * as inquirer from "inquirer";
import * as path from "path";
import { directoryExists } from "../utils/fs";
import { ProductionInstallation } from "../utils/installation";
import { Installation } from "../utils/installation";
import { getServicesFromInstall } from "../install/prompt";
import { yellowInstallCommand } from "./utils";
import { NpmPackage } from "../utils/npm";
import { findNpmPackages, NpmPackage } from "../utils/npm";
import { corePackage } from "../nodecgIOVersions";
import { getNodeCGIODirectory } from "../utils/nodecgInstallation";

/**
* Describes all options for bundle generation a user has answered with inside the inquirer prompt
Expand Down Expand Up @@ -36,16 +37,15 @@ export type CodeLanguage = "typescript" | "javascript";

const kebabCaseRegex = /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/;

export async function promptGenerationOpts(
nodecgDir: string,
install: ProductionInstallation,
): Promise<GenerationOptions> {
export async function promptGenerationOpts(nodecgDir: string, install: Installation): Promise<GenerationOptions> {
const defaultBundleDir = path.join(nodecgDir, "bundles");
// if we are already in a bundle directory we use the name of the directory as a bundle name and the corresponding bundle dir
const inBundleDir = path.dirname(process.cwd()) === defaultBundleDir;
const bundleName = inBundleDir ? path.basename(process.cwd()) : undefined;
const bundleDir = inBundleDir ? path.dirname(process.cwd()) : defaultBundleDir;

const installedPackages = install.dev ? await findNpmPackages(getNodeCGIODirectory(nodecgDir)) : install.packages;

const opts: PromptedGenerationOptions = await inquirer.prompt([
{
type: "input",
Expand Down Expand Up @@ -74,13 +74,20 @@ export async function promptGenerationOpts(
validate: validateVersion,
filter: (ver) => new semver.SemVer(ver),
},
{
type: "checkbox",
name: "services",
message: `Which services would you like to use? (they must be installed through ${yellowInstallCommand} first)`,
choices: getServicesFromInstall(install, install.version),
validate: validateServiceSelection,
},
!install.dev
? {
type: "checkbox",
name: "services",
message: `Which services would you like to use? (they must be installed through ${yellowInstallCommand} first)`,
choices: getServicesFromInstall(installedPackages, install.version),
validate: validateServiceSelection,
}
: {
type: "input",
name: "services",
message: `Which services would you like to use? (comma separated)`,
filter: (servicesString) => servicesString.split(","),
},
{
type: "list",
name: "language",
Expand All @@ -99,7 +106,7 @@ export async function promptGenerationOpts(
},
]);

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

// region prompt validation
Expand Down Expand Up @@ -151,9 +158,10 @@ function validateServiceSelection(services: string[]): true | string {
*/
export function computeGenOptsFields(
opts: PromptedGenerationOptions,
install: ProductionInstallation,
install: Installation,
installedPackages: NpmPackage[],
): GenerationOptions {
const corePkg = install.packages.find((pkg) => pkg.name === corePackage);
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 @@ -162,7 +170,7 @@ export function computeGenOptsFields(
...opts,
corePackage: corePkg,
servicePackages: opts.services.map((svc) => {
const svcPackage = install.packages.find((pkg) => pkg.name.endsWith(svc));
const svcPackage = installedPackages.find((pkg) => pkg.name.endsWith(svc));

if (svcPackage === undefined) {
throw new Error(`Service ${svc} has no corresponding package in the passed installation.`);
Expand Down
1 change: 0 additions & 1 deletion src/generate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as chalk from "chalk";

// Colored commands for logging purposes.
export const yellowInstallCommand = chalk.yellow("nodecg-io install");
export const yellowGenerateCommand = chalk.yellow("nodecg-io generate");

/**
* Writes a file for a bundle.
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ const args = yargs(process.argv.slice(2))
.command(generateModule)
.option("disable-updates", { type: "boolean", description: "Disables check for nodecg-io-cli updates" })
.strict()
.demandCommand();
.demandCommand()
.parserConfiguration({
"dot-notation": false,
})
.parse();

ensureNode12();
(async () => {
const opts = await args.argv;
const opts = await args;
if (!opts["disable-updates"]) {
checkForCliUpdate();
}
Expand Down
48 changes: 43 additions & 5 deletions src/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,58 @@ import { logger } from "../utils/log";
import { requireNpmV7 } from "../utils/npm";
import { findNodeCGDirectory, getNodeCGIODirectory } from "../utils/nodecgInstallation";

export const installModule: CommandModule<unknown, { concurrency: number }> = {
export interface InstallCommandOptions {
"nodecg-io-version"?: string;
service: Array<string | number>;
"all-services": boolean;
docs: boolean;
samples: boolean;
}

export const installModule: CommandModule<unknown, InstallCommandOptions> = {
command: "install",
describe: "installs nodecg-io to your local nodecg installation",
handler: async () => {
handler: async (opts) => {
try {
await install();
await install(opts);
} catch (err) {
logger.error(`Error while installing nodecg-io: ${err}`);
process.exit(1);
}
},
builder: (yargs) =>
yargs
.option("nodecg-io-version", {
type: "string",
description:
'The version of nodecg-io to install. Either "major.minor" for production or "development"',
})
.option("service", {
type: "array",
description:
"The modecg-io services to install alongside the needed components. Only affects production installs.",
default: [],
})
.option("all-services", {
type: "boolean",
description: "Whether to install all available services. Only affects production installs.",
default: false,
})
.option("docs", {
type: "boolean",
description:
"Whether to clone the docs repo into the /docs sub directory. Only available for development installs.",
default: true,
})
.option("samples", {
type: "boolean",
description:
"Whether to add the samples to your NodeCG configuration. Only available for development installs.",
default: false,
}),
};

async function install(): Promise<void> {
async function install(opts: InstallCommandOptions): Promise<void> {
await requireNpmV7();

logger.info("Installing nodecg-io...");
Expand All @@ -33,7 +71,7 @@ async function install(): Promise<void> {
const nodecgIODir = getNodeCGIODirectory(nodecgDir);

let currentInstall = await readInstallInfo(nodecgIODir);
const requestedInstall = await promptForInstallInfo(currentInstall);
const requestedInstall = await promptForInstallInfo(currentInstall, opts);

// 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))) {
Expand Down
Loading