Skip to content

Commit

Permalink
Discovery: Added node runtime. (#5993)
Browse files Browse the repository at this point in the history
* Added runtime command discovery

* Resolved comments

* Added error case to analyse codebase method

* Updated install command

* Reorganized tests and removed unwated promise.resolve stmt

* Added review changes on install command and node version string array

* Changes to node.ts to include additional condions on run script

* Added code comments to types

* Added undefied to return if no cmd

* Added undefied to return if no cmd
  • Loading branch information
svnsairam authored Jun 27, 2023
1 parent 2e8e909 commit 417c4b4
Show file tree
Hide file tree
Showing 3 changed files with 454 additions and 1 deletion.
212 changes: 212 additions & 0 deletions src/frameworks/compose/discover/runtime/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { readOrNull } from "../filesystem";
import { FileSystem, FrameworkSpec, Runtime } from "../types";
import { RuntimeSpec } from "../types";
import { frameworkMatcher } from "../frameworkMatcher";
import { LifecycleCommands } from "../types";
import { Command } from "../types";
import { FirebaseError } from "../../../../error";
import { logger } from "../../../../../src/logger";
import { conjoinOptions } from "../../../utils";

export interface PackageJSON {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
scripts?: Record<string, string>;
engines?: Record<string, string>;
}
type PackageManager = "npm" | "yarn";

const supportedNodeVersions: string[] = ["18"];
const NODE_RUNTIME_ID = "nodejs";
const PACKAGE_JSON = "package.json";
const YARN_LOCK = "yarn.lock";

export class NodejsRuntime implements Runtime {
private readonly runtimeRequiredFiles: string[] = [PACKAGE_JSON];
private readonly contentCache: Record<string, boolean> = {};

// Checks if the codebase is using Node as runtime.
async match(fs: FileSystem): Promise<boolean | null> {
const areAllFilesPresent = await Promise.all(
this.runtimeRequiredFiles.map((file) => fs.exists(file))
);

return areAllFilesPresent.every((present) => present);
}

getRuntimeName(): string {
return NODE_RUNTIME_ID;
}

getNodeImage(engine: Record<string, string> | undefined): string {
// If no version is mentioned explicitly, assuming application is compatible with latest version.
if (!engine || !engine.node) {
return `node:${supportedNodeVersions[supportedNodeVersions.length - 1]}-slim`;
}
const versionNumber = engine.node;

if (!supportedNodeVersions.includes(versionNumber)) {
throw new FirebaseError(
`This integration expects Node version ${conjoinOptions(
supportedNodeVersions,
"or"
)}. You're running version ${versionNumber}, which is not compatible.`
);
}

return `node:${versionNumber}-slim`;
}

async getPackageManager(fs: FileSystem): Promise<PackageManager> {
try {
if (await fs.exists(YARN_LOCK)) {
return "yarn";
}

return "npm";
} catch (error: any) {
logger.error("Failed to check files to identify package manager");
throw error;
}
}

getDependencies(packageJSON: PackageJSON): Record<string, string> {
return { ...packageJSON.dependencies, ...packageJSON.devDependencies };
}

packageManagerInstallCommand(packageManager: PackageManager): string | undefined {
const packages: string[] = [];
if (packageManager === "yarn") {
packages.push("yarn");
}
if (!packages.length) {
return undefined;
}

return `npm install --global ${packages.join(" ")}`;
}

installCommand(fs: FileSystem, packageManager: PackageManager): string {
let installCmd = "npm install";

if (packageManager === "yarn") {
installCmd = "yarn install";
}

return installCmd;
}

async detectedCommands(
packageManager: PackageManager,
scripts: Record<string, string> | undefined,
matchedFramework: FrameworkSpec | null,
fs: FileSystem
): Promise<LifecycleCommands> {
return {
build: this.getBuildCommand(packageManager, scripts, matchedFramework),
dev: this.getDevCommand(packageManager, scripts, matchedFramework),
run: await this.getRunCommand(packageManager, scripts, matchedFramework, fs),
};
}

executeScript(packageManager: string, scriptName: string): string {
return `${packageManager} run ${scriptName}`;
}

executeFrameworkCommand(packageManager: PackageManager, command: Command): Command {
if (packageManager === "npm" || packageManager === "yarn") {
command.cmd = "npx " + command.cmd;
}

return command;
}

getBuildCommand(
packageManager: PackageManager,
scripts: Record<string, string> | undefined,
matchedFramework: FrameworkSpec | null
): Command | undefined {
let buildCommand: Command = { cmd: "" };
if (scripts?.build) {
buildCommand.cmd = this.executeScript(packageManager, "build");
} else if (matchedFramework && matchedFramework.commands?.build) {
buildCommand = matchedFramework.commands.build;
buildCommand = this.executeFrameworkCommand(packageManager, buildCommand);
}

return buildCommand.cmd === "" ? undefined : buildCommand;
}

getDevCommand(
packageManager: PackageManager,
scripts: Record<string, string> | undefined,
matchedFramework: FrameworkSpec | null
): Command | undefined {
let devCommand: Command = { cmd: "", env: { NODE_ENV: "dev" } };
if (scripts?.dev) {
devCommand.cmd = this.executeScript(packageManager, "dev");
} else if (matchedFramework && matchedFramework.commands?.dev) {
devCommand = matchedFramework.commands.dev;
devCommand = this.executeFrameworkCommand(packageManager, devCommand);
}

return devCommand.cmd === "" ? undefined : devCommand;
}

async getRunCommand(
packageManager: PackageManager,
scripts: Record<string, string> | undefined,
matchedFramework: FrameworkSpec | null,
fs: FileSystem
): Promise<Command | undefined> {
let runCommand: Command = { cmd: "", env: { NODE_ENV: "production" } };
if (scripts?.start) {
runCommand.cmd = this.executeScript(packageManager, "start");
} else if (matchedFramework && matchedFramework.commands?.run) {
runCommand = matchedFramework.commands.run;
runCommand = this.executeFrameworkCommand(packageManager, runCommand);
} else if (scripts?.main) {
runCommand.cmd = `node ${scripts.main}`;
} else if (await fs.exists("index.js")) {
runCommand.cmd = `node index.js`;
}

return runCommand.cmd === "" ? undefined : runCommand;
}

async analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec> {
try {
const packageJSONRaw = await readOrNull(fs, PACKAGE_JSON);
let packageJSON: PackageJSON = {};
if (packageJSONRaw) {
packageJSON = JSON.parse(packageJSONRaw) as PackageJSON;
}
const packageManager = await this.getPackageManager(fs);
const nodeImage = this.getNodeImage(packageJSON.engines);
const dependencies = this.getDependencies(packageJSON);
const matchedFramework = await frameworkMatcher(
NODE_RUNTIME_ID,
fs,
allFrameworkSpecs,
dependencies
);

const runtimeSpec: RuntimeSpec = {
id: NODE_RUNTIME_ID,
baseImage: nodeImage,
packageManagerInstallCommand: this.packageManagerInstallCommand(packageManager),
installCommand: this.installCommand(fs, packageManager),
detectedCommands: await this.detectedCommands(
packageManager,
packageJSON.scripts,
matchedFramework,
fs
),
};

return runtimeSpec;
} catch (error: any) {
throw new FirebaseError(`Failed to parse engine: ${error}`);
}
}
}
2 changes: 1 addition & 1 deletion src/frameworks/compose/discover/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface FileSystem {
export interface Runtime {
match(fs: FileSystem): Promise<boolean | null>;
getRuntimeName(): string;
analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec | null>;
analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec>;
}

export interface Command {
Expand Down
Loading

0 comments on commit 417c4b4

Please sign in to comment.