Skip to content

Commit

Permalink
Improve file watching and fix Windows issues (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Oct 24, 2024
1 parent c8f94b4 commit c498696
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 62 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ In previous releases, the name "Hypermode" was used for all three._
- Simplify and polish `modus new` experience [#494](https://github.com/hypermodeinc/modus/pull/494)
- Move hyp settings for local model invocation to env variables [#495](https://github.com/hypermodeinc/modus/pull/495) [#504](https://github.com/hypermodeinc/modus/pull/504)
- Change GraphQL SDK examples to use a generic public GraphQL API [#501](https://github.com/hypermodeinc/modus/pull/501)
- Improve file watching and fix Windows issues [#505](https://github.com/hypermodeinc/modus/pull/505)

## 2024-10-02 - Version 0.12.7

Expand Down
29 changes: 25 additions & 4 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"chokidar": "^4.0.1",
"gradient-string": "^3.0.0",
"ora": "^8.1.0",
"picomatch": "^4.0.2",
"semver": "^7.6.3"
},
"devDependencies": {
"@oclif/test": "^4",
"@types/node": "^22",
"@types/picomatch": "^3.0.1",
"@types/semver": "^7.5.8",
"oclif": "^4",
"ts-node": "^10",
Expand Down
11 changes: 8 additions & 3 deletions cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,21 @@ export default class BuildCommand extends Command {
process.env.FORCE_COLOR = chalk.level.toString();

const results = await withSpinner("Building " + app.name, async () => {
const execOpts = {
cwd: appPath,
env: process.env,
shell: true,
};
switch (app.sdk) {
case SDK.AssemblyScript:
if (!(await fs.exists(path.join(appPath, "node_modules")))) {
const results = await execFileWithExitCode("npm", ["install"], { cwd: appPath, env: process.env, shell: true });
const results = await execFileWithExitCode("npm", ["install"], execOpts);
if (results.exitCode !== 0) {
this.logError("Failed to install dependencies");
return results;
}
}
return await execFileWithExitCode("npx", ["modus-as-build"], { cwd: appPath, env: process.env, shell: true });
return await execFileWithExitCode("npx", ["modus-as-build"], execOpts);
case SDK.Go:
const version = app.sdkVersion || (await vi.getLatestInstalledSdkVersion(app.sdk, true));
if (!version) {
Expand All @@ -81,7 +86,7 @@ export default class BuildCommand extends Command {
this.logError("Modus Go Build tool is not installed");
return;
}
return await execFileWithExitCode(buildTool, ["."], { cwd: appPath, env: process.env });
return await execFileWithExitCode(buildTool, ["."], execOpts);
default:
this.logError("Unsupported SDK");
this.exit(1);
Expand Down
86 changes: 65 additions & 21 deletions cli/src/commands/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { spawn } from "node:child_process";
import path from "node:path";
import os from "node:os";
import chalk from "chalk";
import pm from "picomatch";
import chokidar from "chokidar";

import * as fs from "../../util/fs.js";
import * as vi from "../../util/versioninfo.js";
import * as installer from "../../util/installer.js";
import { SDK } from "../../custom/globals.js";
import { getHeader } from "../../custom/header.js";
import { getAppInfo } from "../../util/appinfo.js";
import { isOnline, withSpinner } from "../../util/index.js";
Expand Down Expand Up @@ -50,10 +52,10 @@ export default class DevCommand extends Command {
aliases: ["no-watch"],
description: "Don't watch app code for changes",
}),
freq: Flags.integer({
delay: Flags.integer({
char: "f",
description: "Frequency to check for changes",
default: 3000,
description: "Delay (in milliseconds) between file change detection and rebuild",
default: 500,
}),
};

Expand Down Expand Up @@ -83,9 +85,9 @@ export default class DevCommand extends Command {
await withSpinner(chalk.dim("Downloading and installing " + sdkText), async (spinner) => {
try {
await installer.installSDK(sdk, sdkVersion);
} catch {
} catch (e) {
spinner.fail(chalk.red(`Failed to download ${sdkText}`));
this.exit(1);
throw e;
}
spinner.succeed(chalk.dim(`Installed ${sdkText}`));
});
Expand All @@ -99,9 +101,9 @@ export default class DevCommand extends Command {
await withSpinner(chalk.dim("Downloading and installing " + runtimeText), async (spinner) => {
try {
await installer.installRuntime(runtimeVersion!);
} catch {
} catch (e) {
spinner.fail(chalk.red("Failed to download " + runtimeText));
this.exit(1);
throw e;
}
spinner.succeed(chalk.dim("Installed " + runtimeText));
});
Expand All @@ -117,9 +119,9 @@ export default class DevCommand extends Command {
await withSpinner(chalk.dim("Downloading and installing " + runtimeText), async (spinner) => {
try {
await installer.installRuntime(version!);
} catch {
} catch (e) {
spinner.fail(chalk.red("Failed to download " + runtimeText));
this.exit(1);
throw e;
}
spinner.succeed(chalk.dim("Installed " + runtimeText));
});
Expand All @@ -143,7 +145,6 @@ export default class DevCommand extends Command {

await BuildCommand.run([appPath, "--no-logo"]);

// read from settings.json if it exists, load env vars into env
const hypSettings = await readHypermodeSettings();

const env = {
Expand All @@ -154,14 +155,14 @@ export default class DevCommand extends Command {
HYP_ORG_ID: hypSettings.orgId,
};

const runtime = spawn(runtimePath, ["-appPath", path.join(appPath, "build")], {
stdio: "inherit",
const child = spawn(runtimePath, ["-appPath", path.join(appPath, "build")], {
stdio: ["inherit", "inherit", "pipe"],
env: env,
});
runtime.on("close", (code) => this.exit(code || 1));
child.stderr.pipe(process.stderr);
child.on("close", (code) => this.exit(code || 1));

if (!flags.nowatch) {
const delay = flags.freq;
let lastModified = 0;
let lastBuild = 0;
let paused = true;
Expand All @@ -174,27 +175,49 @@ export default class DevCommand extends Command {
if (lastBuild > lastModified) {
return;
}

lastBuild = Date.now();

try {
child.stderr.pause();
this.log();
this.log(chalk.magentaBright("Detected change. Rebuilding..."));
this.log();
await BuildCommand.run([appPath, "--no-logo"]);
} catch {}
}, delay);
} catch (e) {
this.log(chalk.magenta("Waiting for more changes..."));
this.log(chalk.dim("Press Ctrl+C at any time to stop the server."));
} finally {
child.stderr.resume();
}
}, flags.delay);

const globs = getGlobsToWatch(sdk);

// NOTE: The built-in fs.watch or fsPromises.watch is insufficient for our needs.
// Instead, we use chokidar for consistent behavior in cross-platform file watching.
const ignoredPaths = [path.join(appPath, "build") + path.sep, path.join(appPath, "node_modules") + path.sep];
const pmOpts: pm.PicomatchOptions = { posixSlashes: true };
chokidar
.watch(appPath, {
ignored: (filePath, stats) => (stats?.isFile() || true) && ignoredPaths.some((p) => path.normalize(filePath).startsWith(p)),
cwd: appPath,
ignored: (filePath, stats) => {
const relativePath = path.relative(appPath, filePath);
if (!stats || !relativePath) return false;

let ignore = false;
if (pm(globs.excluded, pmOpts)(relativePath)) {
ignore = true;
} else if (stats.isFile()) {
ignore = !pm(globs.included, pmOpts)(relativePath);
}

if (process.env.MODUS_DEBUG) {
this.log(chalk.dim(`${ignore ? "ignored: " : "watching:"} ${relativePath}`));
}
return ignore;
},
ignoreInitial: true,
persistent: true,
})
.on("all", async (event, path) => {
.on("all", () => {
lastModified = Date.now();
paused = false;
});
Expand All @@ -205,3 +228,24 @@ export default class DevCommand extends Command {
this.log(chalk.red(" ERROR ") + chalk.dim(": " + message));
}
}

function getGlobsToWatch(sdk: SDK) {
const included: string[] = [];
const excluded: string[] = [".git/**", "build/**"];

switch (sdk) {
case SDK.AssemblyScript:
included.push("**/*.ts", "**/asconfig.json", "**/tsconfig.json", "**/package.json");
excluded.push("node_modules/**");
break;

case SDK.Go:
included.push("**/*.go", "**/go.mod");
excluded.push("**/*_generated.go", "**/*.generated.go", "**/*_test.go");
break;

default:
throw new Error(`Unsupported SDK: ${sdk}`);
}
return { included, excluded };
}
21 changes: 13 additions & 8 deletions cli/src/commands/new/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { execFile } from "../../util/cp.js";
import { isOnline } from "../../util/index.js";
import { GitHubOwner, GitHubRepo, MinGoVersion, MinNodeVersion, MinTinyGoVersion, ModusHomeDir, SDK, parseSDK } from "../../custom/globals.js";
import { withSpinner } from "../../util/index.js";
import { extract } from "../../util/tar.js";
import SDKInstallCommand from "../sdk/install/index.js";
import { getHeader } from "../../custom/header.js";
import * as inquirer from "@inquirer/prompts";
Expand Down Expand Up @@ -105,8 +106,8 @@ export default class NewCommand extends Command {
const dir = flags.dir || "." + path.sep + name;

if (!flags.force) {
const confirmd = await inquirer.confirm({ message: "Continue?", default: true });
if (!confirmd) {
const confirmed = await inquirer.confirm({ message: "Continue?", default: true });
if (!confirmed) {
this.log(chalk.dim("Aborted"));
this.exit(1);
}
Expand All @@ -118,8 +119,8 @@ export default class NewCommand extends Command {

private async createApp(name: string, dir: string, sdk: SDK, template: string, force: boolean, prerelease: boolean) {
if (!force && (await fs.exists(dir))) {
const confirmd = await inquirer.confirm({ message: "Attempting to overwrite a folder that already exists.\nAre you sure you want to continue?", default: false });
if (!confirmd) {
const confirmed = await inquirer.confirm({ message: "Attempting to overwrite a folder that already exists.\nAre you sure you want to continue?", default: false });
if (!confirmed) {
this.log(chalk.dim("Aborted"));
this.exit(1);
}
Expand Down Expand Up @@ -204,11 +205,11 @@ export default class NewCommand extends Command {

let updateSDK = false;
if (!installedSdkVersion) {
const confirmd = inquirer.confirm({
const confirmed = inquirer.confirm({
message: `You do not have the ${sdkText} installed. Would you like to install it now?`,
default: true,
});
if (!confirmd) {
if (!confirmed) {
this.log(chalk.dim("Aborted"));
this.exit(1);
} else {
Expand Down Expand Up @@ -253,6 +254,7 @@ export default class NewCommand extends Command {
await withSpinner("Downloading the Modus Go build tool.", async () => {
await execFile("go", ["install", module], {
cwd: ModusHomeDir,
shell: true,
env: {
...process.env,
GOBIN: sdkPath,
Expand All @@ -272,10 +274,11 @@ export default class NewCommand extends Command {
if (!(await fs.exists(dir))) {
await fs.mkdir(dir, { recursive: true });
}
await execFile("tar", ["-xf", templatesArchive, "-C", dir, "--strip-components=2", `templates/${template}`]);

await extract(templatesArchive, dir, "--strip-components=2", `templates/${template}`);

// Apply SDK-specific modifications
const execOpts = { env: process.env, cwd: dir };
const execOpts = { env: process.env, cwd: dir, shell: true };
switch (sdk) {
case SDK.AssemblyScript:
await execFile("npm", ["pkg", "set", `name=${name}`], execOpts);
Expand Down Expand Up @@ -304,6 +307,7 @@ export default class NewCommand extends Command {
async function getGoVersion(): Promise<string | undefined> {
try {
const result = await execFile("go", ["version"], {
shell: true,
cwd: ModusHomeDir,
env: process.env,
});
Expand All @@ -318,6 +322,7 @@ async function getGoVersion(): Promise<string | undefined> {
async function getTinyGoVersion(): Promise<string | undefined> {
try {
const result = await execFile("tinygo", ["version"], {
shell: true,
cwd: ModusHomeDir,
env: process.env,
});
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/runtime/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ export default class RuntimeInstallCommand extends Command {
await withSpinner(chalk.dim("Downloading and installing " + runtimeText), async (spinner) => {
try {
await installer.installRuntime(version);
} catch {
} catch (e) {
spinner.fail(chalk.red(`Failed to download ${runtimeText}`));
this.exit(1);
throw e;
}
spinner.succeed(chalk.dim(`Installed ${runtimeText}`));
});
Expand Down
Loading

0 comments on commit c498696

Please sign in to comment.