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

Commit b2ff747

Browse files
committed
Refactor package.json and tsconfig.json generation into their own files
1 parent 7bda40b commit b2ff747

File tree

7 files changed

+193
-110
lines changed

7 files changed

+193
-110
lines changed

src/generate/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import CodeBlockWriter from "code-block-writer";
22
import { getServiceClientName } from "../nodecgIOVersions";
33
import { ProductionInstallation } from "../utils/installation";
44
import { CodeLanguage, GenerationOptions } from "./prompt";
5-
import { write } from "./index";
5+
import { writeBundleFile } from "./utils";
66

77
interface ServiceNames {
88
name: string;
@@ -70,7 +70,7 @@ export async function genExtension(opts: GenerationOptions, install: ProductionI
7070
});
7171

7272
const fileExtension = opts.language === "typescript" ? "ts" : "js";
73-
await write(writer.toString(), opts.bundlePath, "extension", `index.${fileExtension}`);
73+
await writeBundleFile(writer.toString(), opts.bundlePath, "extension", `index.${fileExtension}`);
7474
}
7575

7676
function genImport(writer: CodeBlockWriter, symbolToImport: string, packageName: string, lang: CodeLanguage) {

src/generate/index.ts

Lines changed: 7 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,17 @@
11
import { CommandModule } from "yargs";
2-
import * as chalk from "chalk";
3-
import * as path from "path";
42
import * as fs from "fs";
53
import { logger } from "../utils/log";
64
import { directoryExists } from "../utils/fs";
75
import { Installation, ProductionInstallation, readInstallInfo } from "../utils/installation";
86
import { corePackages } from "../nodecgIOVersions";
97
import { GenerationOptions, promptGenerationOpts } from "./prompt";
10-
import { getLatestPackageVersion, runNpmBuild, runNpmInstall } from "../utils/npm";
8+
import { runNpmBuild, runNpmInstall } from "../utils/npm";
119
import { genExtension } from "./extension";
12-
import { findNodeCGDirectory, getNodeCGIODirectory, getNodeCGVersion } from "../utils/nodecgInstallation";
13-
import { SemVer } from "semver";
14-
import { genDashboard, genGraphic, genNodeCGDashboardConfig, genNodeCGGraphicConfig } from "./panel";
15-
16-
const defaultTsConfigJson = {
17-
compilerOptions: {
18-
target: "es2019",
19-
sourceMap: true,
20-
lib: ["es2019"],
21-
alwaysStrict: true,
22-
forceConsistentCasingInFileNames: true,
23-
noFallthroughCasesInSwitch: true,
24-
noImplicitAny: true,
25-
noImplicitReturns: true,
26-
noImplicitThis: true,
27-
strictNullChecks: true,
28-
skipLibCheck: true,
29-
module: "CommonJS",
30-
types: ["node"],
31-
},
32-
};
33-
34-
export const yellowInstallCommand = chalk.yellow("nodecg-io install");
35-
const yellowGenerateCommand = chalk.yellow("nodecg-io generate");
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";
3615

3716
export const generateModule: CommandModule = {
3817
command: "generate",
@@ -123,85 +102,9 @@ export async function generateBundle(
123102
}
124103
}
125104

126-
async function genPackageJson(nodecgDir: string, opts: GenerationOptions): Promise<void> {
127-
const serviceDeps: [string, string][] = opts.servicePackages.map((pkg) => [pkg.name, addSemverCaret(pkg.version)]);
128-
129-
const dependencies: [string, string][] = [["nodecg-io-core", addSemverCaret(opts.corePackage.version)]];
130-
131-
// When we use JS we only need core for requireService etc. and if we TS we also need nodecg, ts, types for node and
132-
// each service for typings.
133-
if (opts.language === "typescript") {
134-
dependencies.push(...serviceDeps);
135-
136-
logger.debug("Fetching latest typescript and @types/node versions...");
137-
const [nodecgVersion, latestNodeTypes, latestTypeScript] = await Promise.all([
138-
getNodeCGVersion(nodecgDir),
139-
getLatestPackageVersion("@types/node"),
140-
getLatestPackageVersion("typescript"),
141-
]);
142-
143-
dependencies.push(
144-
["nodecg", addSemverCaret(nodecgVersion)],
145-
["@types/node", addSemverCaret(latestNodeTypes)],
146-
["typescript", addSemverCaret(latestTypeScript)],
147-
);
148-
dependencies.sort();
149-
}
150-
151-
const content = {
152-
name: opts.bundleName,
153-
version: opts.version.version,
154-
private: true,
155-
nodecg: {
156-
compatibleRange: addSemverCaret("1.4.0"),
157-
bundleDependencies: Object.fromEntries(serviceDeps),
158-
graphics: genNodeCGGraphicConfig(opts),
159-
dashboardPanels: genNodeCGDashboardConfig(opts),
160-
},
161-
// These scripts are for compiling TS and thus are only needed when generating a TS bundle
162-
scripts:
163-
opts.language === "typescript"
164-
? {
165-
build: "tsc -b",
166-
watch: "tsc -b -w",
167-
clean: "tsc -b --clean",
168-
}
169-
: undefined,
170-
dependencies: Object.fromEntries(dependencies),
171-
};
172-
173-
await write(content, opts.bundlePath, "package.json");
174-
}
175-
176-
function addSemverCaret(version: string | SemVer): string {
177-
return `^${version}`;
178-
}
179-
180-
async function genTsConfig(opts: GenerationOptions): Promise<void> {
181-
// Only TS needs its tsconfig.json compiler configuration
182-
if (opts.language === "typescript") {
183-
await write(defaultTsConfigJson, opts.bundlePath, "tsconfig.json");
184-
}
185-
}
186-
187105
async function genGitIgnore(opts: GenerationOptions): Promise<void> {
188106
const languageIgnoredFiles = opts.language === "typescript" ? ["/extension/*.js", "/extension/*.js.map"] : [];
189107
const ignoreEntries = ["/node_modules/", "/.vscode/", "/.idea/", ...languageIgnoredFiles];
190108
const content = ignoreEntries.join("\n");
191-
await write(content, opts.bundlePath, ".gitignore");
192-
}
193-
194-
export async function write(content: string | Record<string, unknown>, ...paths: string[]): Promise<void> {
195-
const finalPath = path.join(...paths);
196-
197-
logger.debug(`Writing file at ${finalPath}`);
198-
199-
// Create directory if missing
200-
const parent = path.dirname(finalPath);
201-
if (!(await directoryExists(parent))) {
202-
await fs.promises.mkdir(parent);
203-
}
204-
205-
const str = typeof content === "string" ? content : JSON.stringify(content, null, 4);
206-
await fs.promises.writeFile(finalPath, str);
109+
await writeBundleFile(content, opts.bundlePath, ".gitignore");
207110
}

src/generate/packageJson.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { GenerationOptions } from "./prompt";
2+
import { logger } from "../utils/log";
3+
import { getNodeCGVersion } from "../utils/nodecgInstallation";
4+
import { getLatestPackageVersion } from "../utils/npm";
5+
import { genNodeCGDashboardConfig, genNodeCGGraphicConfig } from "./panel";
6+
import { SemVer } from "semver";
7+
import { writeBundleFile } from "./utils";
8+
9+
/**
10+
* A dependency on a npm package. First field is the package name and the second field is the version.
11+
*/
12+
type Dependency = [string, string];
13+
14+
/**
15+
* Generates the whole package.json file for the bundle.
16+
*
17+
* @param nodecgDir the directory in which nodecg is installed
18+
* @param opts the options that the user chose for the bundle.
19+
*/
20+
export async function genPackageJson(nodecgDir: string, opts: GenerationOptions): Promise<void> {
21+
const serviceDeps: Dependency[] = opts.servicePackages.map((pkg) => [pkg.name, addSemverCaret(pkg.version)]);
22+
23+
const content = {
24+
name: opts.bundleName,
25+
version: opts.version.version,
26+
private: true,
27+
nodecg: {
28+
compatibleRange: addSemverCaret("1.4.0"),
29+
bundleDependencies: Object.fromEntries(serviceDeps),
30+
graphics: genNodeCGGraphicConfig(opts),
31+
dashboardPanels: genNodeCGDashboardConfig(opts),
32+
},
33+
// These scripts are for compiling TS and thus are only needed when generating a TS bundle
34+
scripts: genScripts(opts),
35+
dependencies: Object.fromEntries(await genDependencies(opts, serviceDeps, nodecgDir)),
36+
};
37+
38+
await writeBundleFile(content, opts.bundlePath, "package.json");
39+
}
40+
41+
/**
42+
* Generates the dependency field for a package.json of a bundle.
43+
*
44+
* @param opts the selected options for bundle generation
45+
* @param serviceDeps the dependencies on service packages
46+
* @param nodecgDir the directory in which nodecg is installed
47+
* @return the dependencies for a bundle with the given options.
48+
*/
49+
async function genDependencies(opts: GenerationOptions, serviceDeps: Dependency[], nodecgDir: string) {
50+
const core = [opts.corePackage.name, addSemverCaret(opts.corePackage.version)];
51+
52+
if (opts.language === "typescript") {
53+
// For typescript we need core, all services (for typings) and special packages like ts itself or node typings.
54+
const deps = [core, ...serviceDeps, ...(await genTypeScriptDependencies(nodecgDir))];
55+
deps.sort();
56+
return deps;
57+
} else {
58+
// For JS we only need the core for e.g. the requireService function.
59+
return [core];
60+
}
61+
}
62+
63+
/**
64+
* Generates all extra dependencies that are needed when having a bundle in TS. Meaning typescript itself, nodecg for typings
65+
* and types for node.
66+
* @param nodecgDir the directory in which nodecg is installed. Used to get nodecg version which will be used by nodecg dependency.
67+
*/
68+
async function genTypeScriptDependencies(nodecgDir: string): Promise<Dependency[]> {
69+
logger.debug("Fetching latest typescript and @types/node versions...");
70+
const [nodecgVersion, latestNodeTypes, latestTypeScript] = await Promise.all([
71+
getNodeCGVersion(nodecgDir),
72+
getLatestPackageVersion("@types/node"),
73+
getLatestPackageVersion("typescript"),
74+
]);
75+
76+
return [
77+
["nodecg", addSemverCaret(nodecgVersion)],
78+
["@types/node", addSemverCaret(latestNodeTypes)],
79+
["typescript", addSemverCaret(latestTypeScript)],
80+
];
81+
}
82+
83+
/**
84+
* Generates the script field of the package.json.
85+
* Contains build scripts for TypeScript if it was chosen as a bundle language.
86+
* Will be empty for JavaScript because it doesn't need to be built.
87+
*/
88+
function genScripts(opts: GenerationOptions) {
89+
if (opts.language !== "typescript") {
90+
return undefined;
91+
}
92+
93+
// Is TypeScript, thus we need the build scripts that invoke tsc
94+
return {
95+
build: "tsc -b",
96+
watch: "tsc -b -w",
97+
clean: "tsc -b --clean",
98+
};
99+
}
100+
101+
/**
102+
* Adds the semver caret operator to a given version to allow or minor and patch updates by npm.
103+
*
104+
* @param version the base version
105+
* @return the version with the semver caret operator in front.
106+
*/
107+
function addSemverCaret(version: string | SemVer): string {
108+
return `^${version}`;
109+
}

src/generate/panel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GenerationOptions } from "./prompt";
2-
import { write } from "./index";
2+
import { writeBundleFile } from "./utils";
33

44
type PanelType = "graphic" | "dashboard";
55

@@ -42,13 +42,13 @@ function genPanel(type: PanelType, opts: GenerationOptions): string {
4242

4343
export async function genGraphic(opts: GenerationOptions): Promise<void> {
4444
if (opts.graphic) {
45-
await write(genPanel("graphic", opts), opts.bundlePath, "graphics", graphicFileName);
45+
await writeBundleFile(genPanel("graphic", opts), opts.bundlePath, "graphics", graphicFileName);
4646
}
4747
}
4848

4949
export async function genDashboard(opts: GenerationOptions): Promise<void> {
5050
if (opts.dashboard) {
51-
await write(genPanel("dashboard", opts), opts.bundlePath, "dashboard", dashboardFile);
51+
await writeBundleFile(genPanel("dashboard", opts), opts.bundlePath, "dashboard", dashboardFile);
5252
}
5353
}
5454

src/generate/prompt.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as path from "path";
44
import { directoryExists } from "../utils/fs";
55
import { ProductionInstallation } from "../utils/installation";
66
import { getServicesFromInstall } from "../install/prompt";
7-
import { yellowInstallCommand } from ".";
7+
import { yellowInstallCommand } from "./utils";
88
import { NpmPackage } from "../utils/npm";
99
import { corePackage } from "../nodecgIOVersions";
1010

@@ -101,6 +101,8 @@ export async function promptGenerationOpts(
101101
return computeGenOptsFields(opts, install);
102102
}
103103

104+
// region prompt validation
105+
104106
function validateBundleName(str: string): true | string {
105107
if (str.length === 0) {
106108
return "You must provide a bundle name.";
@@ -136,6 +138,16 @@ function validateServiceSelection(services: string[]): true | string {
136138
}
137139
}
138140

141+
// endregion
142+
143+
/**
144+
* Converts a {@link PromptedGenerationOptions} to a {@link GenerationOptions} by computing the fields
145+
* that {@link GenerationOptions} has using the passed installation.
146+
*
147+
* @param opts the prompted generation options
148+
* @param install the current nodecg-io installation. Used to get installed packages/exact versions.
149+
* @return opts including computed fields.
150+
*/
139151
export function computeGenOptsFields(
140152
opts: PromptedGenerationOptions,
141153
install: ProductionInstallation,

src/generate/tsConfig.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { GenerationOptions } from "./prompt";
2+
import { writeBundleFile } from "./utils";
3+
4+
const defaultTsConfigJson = {
5+
compilerOptions: {
6+
target: "es2019",
7+
sourceMap: true,
8+
lib: ["es2019"],
9+
alwaysStrict: true,
10+
forceConsistentCasingInFileNames: true,
11+
noFallthroughCasesInSwitch: true,
12+
noImplicitAny: true,
13+
noImplicitReturns: true,
14+
noImplicitThis: true,
15+
strictNullChecks: true,
16+
skipLibCheck: true,
17+
module: "CommonJS",
18+
types: ["node"],
19+
},
20+
};
21+
22+
/**
23+
* Generates a tsconfig.json for a bundle if the language was set to typescript.
24+
*/
25+
export async function genTsConfig(opts: GenerationOptions): Promise<void> {
26+
// Only TS needs its tsconfig.json compiler configuration
27+
if (opts.language === "typescript") {
28+
await writeBundleFile(defaultTsConfigJson, opts.bundlePath, "tsconfig.json");
29+
}
30+
}

src/generate/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as path from "path";
2+
import { logger } from "../utils/log";
3+
import { directoryExists } from "../utils/fs";
4+
import * as fs from "fs";
5+
import * as chalk from "chalk";
6+
7+
// Colored commands for logging purposes.
8+
export const yellowInstallCommand = chalk.yellow("nodecg-io install");
9+
export const yellowGenerateCommand = chalk.yellow("nodecg-io generate");
10+
11+
/**
12+
* Writes a file for a bundle.
13+
* @param content the file content. A object will automatically be converted to a string and pretty printed.
14+
* @param paths the path to the file. You should probably start with the bundle path and add directories/filenames.
15+
*/
16+
export async function writeBundleFile(content: string | Record<string, unknown>, ...paths: string[]): Promise<void> {
17+
const finalPath = path.join(...paths);
18+
19+
logger.debug(`Writing file at ${finalPath}`);
20+
21+
// Create directory if missing
22+
const parent = path.dirname(finalPath);
23+
if (!(await directoryExists(parent))) {
24+
await fs.promises.mkdir(parent);
25+
}
26+
27+
const str = typeof content === "string" ? content : JSON.stringify(content, null, 4);
28+
await fs.promises.writeFile(finalPath, str);
29+
}

0 commit comments

Comments
 (0)