Skip to content

Commit 417c4b4

Browse files
authored
Discovery: Added node runtime. (#5993)
* 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
1 parent 2e8e909 commit 417c4b4

File tree

3 files changed

+454
-1
lines changed

3 files changed

+454
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { readOrNull } from "../filesystem";
2+
import { FileSystem, FrameworkSpec, Runtime } from "../types";
3+
import { RuntimeSpec } from "../types";
4+
import { frameworkMatcher } from "../frameworkMatcher";
5+
import { LifecycleCommands } from "../types";
6+
import { Command } from "../types";
7+
import { FirebaseError } from "../../../../error";
8+
import { logger } from "../../../../../src/logger";
9+
import { conjoinOptions } from "../../../utils";
10+
11+
export interface PackageJSON {
12+
dependencies?: Record<string, string>;
13+
devDependencies?: Record<string, string>;
14+
scripts?: Record<string, string>;
15+
engines?: Record<string, string>;
16+
}
17+
type PackageManager = "npm" | "yarn";
18+
19+
const supportedNodeVersions: string[] = ["18"];
20+
const NODE_RUNTIME_ID = "nodejs";
21+
const PACKAGE_JSON = "package.json";
22+
const YARN_LOCK = "yarn.lock";
23+
24+
export class NodejsRuntime implements Runtime {
25+
private readonly runtimeRequiredFiles: string[] = [PACKAGE_JSON];
26+
private readonly contentCache: Record<string, boolean> = {};
27+
28+
// Checks if the codebase is using Node as runtime.
29+
async match(fs: FileSystem): Promise<boolean | null> {
30+
const areAllFilesPresent = await Promise.all(
31+
this.runtimeRequiredFiles.map((file) => fs.exists(file))
32+
);
33+
34+
return areAllFilesPresent.every((present) => present);
35+
}
36+
37+
getRuntimeName(): string {
38+
return NODE_RUNTIME_ID;
39+
}
40+
41+
getNodeImage(engine: Record<string, string> | undefined): string {
42+
// If no version is mentioned explicitly, assuming application is compatible with latest version.
43+
if (!engine || !engine.node) {
44+
return `node:${supportedNodeVersions[supportedNodeVersions.length - 1]}-slim`;
45+
}
46+
const versionNumber = engine.node;
47+
48+
if (!supportedNodeVersions.includes(versionNumber)) {
49+
throw new FirebaseError(
50+
`This integration expects Node version ${conjoinOptions(
51+
supportedNodeVersions,
52+
"or"
53+
)}. You're running version ${versionNumber}, which is not compatible.`
54+
);
55+
}
56+
57+
return `node:${versionNumber}-slim`;
58+
}
59+
60+
async getPackageManager(fs: FileSystem): Promise<PackageManager> {
61+
try {
62+
if (await fs.exists(YARN_LOCK)) {
63+
return "yarn";
64+
}
65+
66+
return "npm";
67+
} catch (error: any) {
68+
logger.error("Failed to check files to identify package manager");
69+
throw error;
70+
}
71+
}
72+
73+
getDependencies(packageJSON: PackageJSON): Record<string, string> {
74+
return { ...packageJSON.dependencies, ...packageJSON.devDependencies };
75+
}
76+
77+
packageManagerInstallCommand(packageManager: PackageManager): string | undefined {
78+
const packages: string[] = [];
79+
if (packageManager === "yarn") {
80+
packages.push("yarn");
81+
}
82+
if (!packages.length) {
83+
return undefined;
84+
}
85+
86+
return `npm install --global ${packages.join(" ")}`;
87+
}
88+
89+
installCommand(fs: FileSystem, packageManager: PackageManager): string {
90+
let installCmd = "npm install";
91+
92+
if (packageManager === "yarn") {
93+
installCmd = "yarn install";
94+
}
95+
96+
return installCmd;
97+
}
98+
99+
async detectedCommands(
100+
packageManager: PackageManager,
101+
scripts: Record<string, string> | undefined,
102+
matchedFramework: FrameworkSpec | null,
103+
fs: FileSystem
104+
): Promise<LifecycleCommands> {
105+
return {
106+
build: this.getBuildCommand(packageManager, scripts, matchedFramework),
107+
dev: this.getDevCommand(packageManager, scripts, matchedFramework),
108+
run: await this.getRunCommand(packageManager, scripts, matchedFramework, fs),
109+
};
110+
}
111+
112+
executeScript(packageManager: string, scriptName: string): string {
113+
return `${packageManager} run ${scriptName}`;
114+
}
115+
116+
executeFrameworkCommand(packageManager: PackageManager, command: Command): Command {
117+
if (packageManager === "npm" || packageManager === "yarn") {
118+
command.cmd = "npx " + command.cmd;
119+
}
120+
121+
return command;
122+
}
123+
124+
getBuildCommand(
125+
packageManager: PackageManager,
126+
scripts: Record<string, string> | undefined,
127+
matchedFramework: FrameworkSpec | null
128+
): Command | undefined {
129+
let buildCommand: Command = { cmd: "" };
130+
if (scripts?.build) {
131+
buildCommand.cmd = this.executeScript(packageManager, "build");
132+
} else if (matchedFramework && matchedFramework.commands?.build) {
133+
buildCommand = matchedFramework.commands.build;
134+
buildCommand = this.executeFrameworkCommand(packageManager, buildCommand);
135+
}
136+
137+
return buildCommand.cmd === "" ? undefined : buildCommand;
138+
}
139+
140+
getDevCommand(
141+
packageManager: PackageManager,
142+
scripts: Record<string, string> | undefined,
143+
matchedFramework: FrameworkSpec | null
144+
): Command | undefined {
145+
let devCommand: Command = { cmd: "", env: { NODE_ENV: "dev" } };
146+
if (scripts?.dev) {
147+
devCommand.cmd = this.executeScript(packageManager, "dev");
148+
} else if (matchedFramework && matchedFramework.commands?.dev) {
149+
devCommand = matchedFramework.commands.dev;
150+
devCommand = this.executeFrameworkCommand(packageManager, devCommand);
151+
}
152+
153+
return devCommand.cmd === "" ? undefined : devCommand;
154+
}
155+
156+
async getRunCommand(
157+
packageManager: PackageManager,
158+
scripts: Record<string, string> | undefined,
159+
matchedFramework: FrameworkSpec | null,
160+
fs: FileSystem
161+
): Promise<Command | undefined> {
162+
let runCommand: Command = { cmd: "", env: { NODE_ENV: "production" } };
163+
if (scripts?.start) {
164+
runCommand.cmd = this.executeScript(packageManager, "start");
165+
} else if (matchedFramework && matchedFramework.commands?.run) {
166+
runCommand = matchedFramework.commands.run;
167+
runCommand = this.executeFrameworkCommand(packageManager, runCommand);
168+
} else if (scripts?.main) {
169+
runCommand.cmd = `node ${scripts.main}`;
170+
} else if (await fs.exists("index.js")) {
171+
runCommand.cmd = `node index.js`;
172+
}
173+
174+
return runCommand.cmd === "" ? undefined : runCommand;
175+
}
176+
177+
async analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec> {
178+
try {
179+
const packageJSONRaw = await readOrNull(fs, PACKAGE_JSON);
180+
let packageJSON: PackageJSON = {};
181+
if (packageJSONRaw) {
182+
packageJSON = JSON.parse(packageJSONRaw) as PackageJSON;
183+
}
184+
const packageManager = await this.getPackageManager(fs);
185+
const nodeImage = this.getNodeImage(packageJSON.engines);
186+
const dependencies = this.getDependencies(packageJSON);
187+
const matchedFramework = await frameworkMatcher(
188+
NODE_RUNTIME_ID,
189+
fs,
190+
allFrameworkSpecs,
191+
dependencies
192+
);
193+
194+
const runtimeSpec: RuntimeSpec = {
195+
id: NODE_RUNTIME_ID,
196+
baseImage: nodeImage,
197+
packageManagerInstallCommand: this.packageManagerInstallCommand(packageManager),
198+
installCommand: this.installCommand(fs, packageManager),
199+
detectedCommands: await this.detectedCommands(
200+
packageManager,
201+
packageJSON.scripts,
202+
matchedFramework,
203+
fs
204+
),
205+
};
206+
207+
return runtimeSpec;
208+
} catch (error: any) {
209+
throw new FirebaseError(`Failed to parse engine: ${error}`);
210+
}
211+
}
212+
}

src/frameworks/compose/discover/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface FileSystem {
66
export interface Runtime {
77
match(fs: FileSystem): Promise<boolean | null>;
88
getRuntimeName(): string;
9-
analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec | null>;
9+
analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise<RuntimeSpec>;
1010
}
1111

1212
export interface Command {

0 commit comments

Comments
 (0)