Skip to content

Commit

Permalink
feat(cli): --watch / -w option
Browse files Browse the repository at this point in the history
  • Loading branch information
Gusarich committed Feb 15, 2025
1 parent 1ee16d1 commit e25d959
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 4 deletions.
34 changes: 30 additions & 4 deletions src/cli/tact/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import type { TactErrorCollection } from "../../error/errors";
import files from "../../stdlib/stdlib";
import { cwd } from "process";
import { getVersion, showCommit } from "../version";
import { watchAndCompile } from "./watch";

export type Args = ArgConsumer<GetParserResult<ReturnType<typeof ArgSchema>>>;

export const main = async () => {
const Log = CliLogger();
Expand Down Expand Up @@ -63,6 +66,7 @@ const ArgSchema = (Parser: ArgParser) => {
.add(Parser.boolean("version", "v"))
.add(Parser.boolean("help", "h"))
.add(Parser.string("output", "o", "DIR"))
.add(Parser.boolean("watch", "w"))
.add(Parser.immediate).end;
};

Expand All @@ -81,6 +85,7 @@ Flags
-o, --output DIR Specify output directory for compiled files
-v, --version Print Tact compiler version and exit
-h, --help Display this text and exit
-w, --watch Watch for changes and recompile
Examples
$ tact --version
Expand All @@ -91,8 +96,6 @@ Join Telegram group: https://t.me/tactlang
Follow X/Twitter account: https://twitter.com/tact_language`);
};

type Args = ArgConsumer<GetParserResult<ReturnType<typeof ArgSchema>>>;

const parseArgs = async (Errors: CliErrors, Args: Args) => {
if (Args.single("help")) {
if (await noUnknownParams(Errors, Args)) {
Expand Down Expand Up @@ -137,7 +140,19 @@ const parseArgs = async (Errors: CliErrors, Args: Args) => {
if (!config) {
return;
}
await compile(Args, Errors, Fs, config);

if (Args.single("watch")) {
await watchAndCompile(
Args,
Errors,
Fs,
config,
normalizedDirPath,
compile,
);
} else {
await compile(Args, Errors, Fs, config);
}
return;
}

Expand All @@ -157,7 +172,18 @@ const parseArgs = async (Errors: CliErrors, Args: Args) => {
relativeOutputDir,
);

await compile(Args, Errors, Fs, config);
if (Args.single("watch")) {
await watchAndCompile(
Args,
Errors,
Fs,
config,
normalizedPath,
compile,
);
} else {
await compile(Args, Errors, Fs, config);
}
return;
}

Expand Down
137 changes: 137 additions & 0 deletions src/cli/tact/watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { watch } from "fs/promises";
import { join } from "path";
import { createNodeFileSystem } from "../../vfs/createNodeFileSystem";
import type { VirtualFileSystem } from "../../vfs/VirtualFileSystem";
import type { Config } from "../../config/parseConfig";
import { Logger, LogLevel } from "../../context/logger";
import type { CliErrors } from "./error-schema";
import type { Args } from "./index";
import { parseConfig } from "../../config/parseConfig";
import { ZodError } from "zod";

let currentCompilation: Promise<void> | null = null;
let abortController: AbortController | null = null;

export const watchAndCompile = async (
Args: Args,
Errors: CliErrors,
Fs: VirtualFileSystem,
config: Config,
watchPath: string,
compile: (
Args: Args,
Errors: CliErrors,
Fs: VirtualFileSystem,
config: Config,
signal?: AbortSignal,
) => Promise<void>,
) => {
const logger = new Logger(
Args.single("quiet") ? LogLevel.NONE : LogLevel.INFO,
);
logger.info("👀 Watching for changes...");

try {
const watcher = watch(watchPath, { recursive: true });

// Initial compilation
currentCompilation = compile(Args, Errors, Fs, config);
await currentCompilation;
logger.info("✅ Initial build completed successfully");

for await (const event of watcher) {
if (
event.filename?.endsWith(".tact") ||
event.filename === "tact.config.json"
) {
logger.info(
`🔄 Change detected in ${event.filename}, rebuilding...`,
);

// Cancel previous compilation if it's still running
if (abortController) {
abortController.abort();
}

// Create new abort controller for this compilation
abortController = new AbortController();

// Small delay to handle multiple rapid changes
await new Promise((resolve) => setTimeout(resolve, 100));

try {
// Create a fresh file system instance using the original root
const freshFs = createNodeFileSystem(Fs.root, false);

// If it's a config file, reload the config
if (event.filename === "tact.config.json") {
const configPath = join(watchPath, "tact.config.json");
if (!freshFs.exists(configPath)) {
logger.error(
"❌ Config file not found after change",
);
continue;
}
const configText = freshFs
.readFile(configPath)
.toString("utf-8");
try {
const newConfig = parseConfig(configText);
config = newConfig;
} catch (e) {
if (e instanceof ZodError) {
logger.error(
`❌ Config error: ${e.toString()}`,
);
} else {
throw e;
}
continue;
}
}

// For .tact files, verify the file exists and is readable
if (event.filename.endsWith(".tact")) {
const filePath = join(watchPath, event.filename);
if (!freshFs.exists(filePath)) {
logger.error(
`❌ File not found after change: ${event.filename}`,
);
continue;
}
}

currentCompilation = compile(
Args,
Errors,
freshFs,
config,
abortController.signal,
);
await currentCompilation;
logger.info("✅ Build completed successfully");
} catch (error: unknown) {
if (error instanceof Error) {
if (
error.name === "AbortError" ||
error.message === "AbortError"
) {
logger.info("🛑 Build cancelled");
} else {
logger.error(`❌ Build failed: ${error.message}`);
}
} else {
logger.error("❌ Build failed with unknown error");
}
}
}
}
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`❌ Watch mode error: ${error.message}`);
} else {
logger.error("❌ Watch mode error: Unknown error occurred");
}
process.exit(1);
}
};

0 comments on commit e25d959

Please sign in to comment.