Skip to content

Commit f107067

Browse files
committed
feat: Add dart delegate
1 parent 7124ad0 commit f107067

File tree

7 files changed

+429
-32
lines changed

7 files changed

+429
-32
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { promisify } from "util";
4+
import { ChildProcess } from "child_process";
5+
import * as spawn from "cross-spawn";
6+
7+
import * as runtimes from "..";
8+
import * as backend from "../../backend";
9+
import * as discovery from "../discovery";
10+
import * as supported from "../supported";
11+
import { logger } from "../../../../logger";
12+
import { FirebaseError } from "../../../../error";
13+
import { Build } from "../../build";
14+
15+
/**
16+
* Create a runtime delegate for the Dart runtime, if applicable.
17+
* @param context runtimes.DelegateContext
18+
* @return Delegate Dart runtime delegate
19+
*/
20+
export async function tryCreateDelegate(
21+
context: runtimes.DelegateContext,
22+
): Promise<Delegate | undefined> {
23+
const pubspecYamlPath = path.join(context.sourceDir, "pubspec.yaml");
24+
25+
if (!(await promisify(fs.exists)(pubspecYamlPath))) {
26+
logger.debug("Customer code is not Dart code.");
27+
return;
28+
}
29+
const runtime = context.runtime ?? supported.latest("dart");
30+
if (!supported.isRuntime(runtime)) {
31+
throw new FirebaseError(`Runtime ${runtime as string} is not a valid Dart runtime`);
32+
}
33+
if (!supported.runtimeIsLanguage(runtime, "dart")) {
34+
throw new FirebaseError(
35+
`Internal error. Trying to construct a dart runtime delegate for runtime ${runtime}`,
36+
{ exit: 1 },
37+
);
38+
}
39+
return Promise.resolve(new Delegate(context.projectId, context.sourceDir, runtime));
40+
}
41+
42+
export class Delegate implements runtimes.RuntimeDelegate {
43+
public readonly language = "dart";
44+
constructor(
45+
private readonly projectId: string,
46+
private readonly sourceDir: string,
47+
public readonly runtime: supported.Runtime & supported.RuntimeOf<"dart">,
48+
) {}
49+
50+
private _bin = "";
51+
52+
get bin(): string {
53+
if (this._bin === "") {
54+
this._bin = "dart";
55+
}
56+
return this._bin;
57+
}
58+
59+
async validate(): Promise<void> {
60+
// Basic validation: check that pubspec.yaml exists and is readable
61+
const pubspecYamlPath = path.join(this.sourceDir, "pubspec.yaml");
62+
try {
63+
await fs.promises.access(pubspecYamlPath, fs.constants.R_OK);
64+
// TODO: could add more validation like checking for firebase_functions dependency
65+
} catch (err: any) {
66+
throw new FirebaseError(
67+
`Failed to read pubspec.yaml at ${pubspecYamlPath}: ${err.message}`,
68+
);
69+
}
70+
}
71+
72+
async build(): Promise<void> {
73+
// No-op: build_runner handles building
74+
return Promise.resolve();
75+
}
76+
77+
watch(): Promise<() => Promise<void>> {
78+
const dartRunProcess = spawn(this.bin, ["run", this.sourceDir], {
79+
cwd: this.sourceDir,
80+
stdio: ["ignore", "pipe", "pipe"],
81+
});
82+
83+
const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "watch", "-d"], {
84+
cwd: this.sourceDir,
85+
stdio: ["ignore", "pipe", "pipe"],
86+
});
87+
88+
// Log output from both processes
89+
dartRunProcess.stdout?.on("data", (chunk: Buffer) => {
90+
logger.info(`[dart run] ${chunk.toString("utf8")}`);
91+
});
92+
dartRunProcess.stderr?.on("data", (chunk: Buffer) => {
93+
logger.error(`[dart run] ${chunk.toString("utf8")}`);
94+
});
95+
96+
buildRunnerProcess.stdout?.on("data", (chunk: Buffer) => {
97+
logger.info(`[build_runner] ${chunk.toString("utf8")}`);
98+
});
99+
buildRunnerProcess.stderr?.on("data", (chunk: Buffer) => {
100+
logger.error(`[build_runner] ${chunk.toString("utf8")}`);
101+
});
102+
103+
// Return cleanup function
104+
return Promise.resolve(async () => {
105+
const killProcess = (proc: ChildProcess) => {
106+
if (!proc.killed && proc.exitCode === null) {
107+
proc.kill("SIGTERM");
108+
}
109+
};
110+
111+
// Try graceful shutdown first
112+
killProcess(dartRunProcess);
113+
killProcess(buildRunnerProcess);
114+
115+
// Wait a bit for graceful shutdown
116+
await new Promise((resolve) => setTimeout(resolve, 2000));
117+
118+
// Force kill if still running
119+
if (!dartRunProcess.killed && dartRunProcess.exitCode === null) {
120+
dartRunProcess.kill("SIGKILL");
121+
}
122+
if (!buildRunnerProcess.killed && buildRunnerProcess.exitCode === null) {
123+
buildRunnerProcess.kill("SIGKILL");
124+
}
125+
126+
// Wait for both processes to exit
127+
await Promise.all([
128+
new Promise<void>((resolve) => {
129+
if (dartRunProcess.killed || dartRunProcess.exitCode !== null) {
130+
resolve();
131+
} else {
132+
dartRunProcess.once("exit", () => resolve());
133+
}
134+
}),
135+
new Promise<void>((resolve) => {
136+
if (buildRunnerProcess.killed || buildRunnerProcess.exitCode !== null) {
137+
resolve();
138+
} else {
139+
buildRunnerProcess.once("exit", () => resolve());
140+
}
141+
}),
142+
]);
143+
});
144+
}
145+
146+
async discoverBuild(
147+
_configValues: backend.RuntimeConfigValues,
148+
_envs: backend.EnvironmentVariables,
149+
): Promise<Build> {
150+
// Use file-based discovery from .dart_tool/firebase/functions.yaml
151+
const yamlDir = path.join(this.sourceDir, ".dart_tool", "firebase");
152+
const yamlPath = path.join(yamlDir, "functions.yaml");
153+
let discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime);
154+
155+
if (!discovered) {
156+
// If the file doesn't exist yet, run build_runner to generate it
157+
logger.debug("functions.yaml not found, running build_runner to generate it...");
158+
const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "build"], {
159+
cwd: this.sourceDir,
160+
stdio: ["ignore", "pipe", "pipe"],
161+
});
162+
163+
// Log build_runner output
164+
buildRunnerProcess.stdout?.on("data", (chunk: Buffer) => {
165+
logger.debug(`[build_runner] ${chunk.toString("utf8")}`);
166+
});
167+
buildRunnerProcess.stderr?.on("data", (chunk: Buffer) => {
168+
logger.debug(`[build_runner] ${chunk.toString("utf8")}`);
169+
});
170+
171+
await new Promise<void>((resolve, reject) => {
172+
buildRunnerProcess.on("exit", (code) => {
173+
if (code === 0 || code === null) {
174+
resolve();
175+
} else {
176+
reject(
177+
new FirebaseError(
178+
`build_runner failed with exit code ${code}. Make sure your Dart project is properly configured.`,
179+
),
180+
);
181+
}
182+
});
183+
buildRunnerProcess.on("error", reject);
184+
});
185+
186+
// Try to discover again after build_runner completes
187+
discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime);
188+
if (!discovered) {
189+
throw new FirebaseError(
190+
`Could not find functions.yaml at ${yamlPath} after running build_runner. ` +
191+
`Make sure your Dart project is properly configured with firebase_functions.`,
192+
);
193+
}
194+
}
195+
196+
return discovered;
197+
}
198+
}

src/deploy/functions/runtimes/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as backend from "../backend";
22
import * as build from "../build";
3+
import * as dart from "./dart";
34
import * as node from "./node";
45
import * as python from "./python";
56
import * as validate from "../validate";
@@ -70,7 +71,7 @@ export interface DelegateContext {
7071
}
7172

7273
type Factory = (context: DelegateContext) => Promise<RuntimeDelegate | undefined>;
73-
const factories: Factory[] = [node.tryCreateDelegate, python.tryCreateDelegate];
74+
const factories: Factory[] = [node.tryCreateDelegate, python.tryCreateDelegate, dart.tryCreateDelegate];
7475

7576
/**
7677
* Gets the delegate object responsible for discovering, building, and hosting

src/deploy/functions/runtimes/supported/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type RuntimeStatus = "experimental" | "beta" | "GA" | "deprecated" | "dec
99
type Day = `${number}-${number}-${number}`;
1010

1111
/** Supported languages. All Runtime are a language + version. */
12-
export type Language = "nodejs" | "python";
12+
export type Language = "nodejs" | "python" | "dart";
1313

1414
/**
1515
* Helper type that is more friendlier than string interpolation everywhere.
@@ -113,6 +113,13 @@ export const RUNTIMES = runtimes({
113113
deprecationDate: "2029-10-10",
114114
decommissionDate: "2030-04-10",
115115
},
116+
dart3: {
117+
friendly: "Dart 3",
118+
status: "GA",
119+
// TODO: Check these
120+
deprecationDate: "2027-10-01",
121+
decommissionDate: "2028-04-01",
122+
},
116123
});
117124

118125
export type Runtime = keyof typeof RUNTIMES & RuntimeOf<Language>;

0 commit comments

Comments
 (0)