Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ JavaScript 設定、最終 MPK ビルドを担当します。
| `gleam run -m glendix/lint_fix` | lint 修正 |
| `gleam run -m glendix/release` | release ビルド |

### コマンド実行境界

`glendix/cmd.exec` は stdin、stdout、stderr を継承する同期 shell command
API を維持します。一般的な process 実行は `shellout` で実装し、小さな
platform adapter が Unix では `/bin/sh -c`、Windows では
`ComSpec /d /s /c` を選択することで、既存の command string と shell
operator の挙動を保ちます。`plinth/node/child_process` は、現在の API が同期
完了、exit status、標準 stream option を typed result として提供しないため、
この境界には使用しません。

`cmd_ffi.mjs` に残る custom command tooling は、意図的に Glendix 固有です。
bridge の生成と cleanup、開発 watcher、experimental-native runtime 設定、
Node/npm 互換 shim、Rollup/WebAssembly 処理が含まれます。Gleam build
command も限定的な filtered runner を維持します。`shellout` では既存の stream
継承を保ちながら stderr だけを capture して filter できないためです。

## 6.0.0 の破壊的変更

`glendix/js/array` は変換を `gleam/javascript/array` に委譲するようになり、手書き
Expand Down
15 changes: 15 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,21 @@ JavaScript 설정, 최종 MPK 빌드를 담당한다. Marketplace 위젯을 쓰
| `gleam run -m glendix/lint_fix` | lint 수정 |
| `gleam run -m glendix/release` | release 빌드 |

### 명령 실행 경계

`glendix/cmd.exec`는 stdin, stdout, stderr를 상속하는 동기식 shell command
API를 유지한다. 일반 process 실행은 `shellout`으로 구현하고, 작은 platform
adapter가 Unix에서는 `/bin/sh -c`, Windows에서는 `ComSpec /d /s /c`를 선택해
기존 command string과 shell operator의 동작을 보존한다.
`plinth/node/child_process`는 현재 API가 동기 완료, exit status, 표준 stream
option을 typed result로 제공하지 않으므로 이 경계에는 사용하지 않는다.

`cmd_ffi.mjs`에 남은 custom command tooling은 의도적으로 Glendix에 특화되어
있다. 여기에는 bridge 생성과 정리, 개발 watcher, experimental-native runtime
설정, Node/npm 호환 shim, Rollup/WebAssembly 처리가 포함된다. Gleam build
command도 좁은 filtered runner를 유지한다. `shellout`은 기존의 stream 상속
동작을 보존하면서 stderr만 별도로 capture하고 filter할 수 없기 때문이다.

## 6.0.0 호환성 변경

`glendix/js/array`는 이제 변환을 `gleam/javascript/array`에 위임하며 수기
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,23 @@ Projects that do not use Marketplace widgets omit the first two steps.
| `gleam run -m glendix/lint_fix` | Apply lint fixes |
| `gleam run -m glendix/release` | Run the release build |

### Command execution boundary

`glendix/cmd.exec` remains a synchronous shell-command API with inherited
stdin, stdout, and stderr. Its generic process execution is implemented with
`shellout`; a small platform adapter selects `/bin/sh -c` on Unix systems and
`ComSpec /d /s /c` on Windows so existing command strings and shell operators
keep their behavior. `plinth/node/child_process` is not used here because its
current API does not provide synchronous completion, exit status, and standard
stream options as a typed result.

The custom command tooling retained in `cmd_ffi.mjs` is intentionally
Glendix-specific: bridge generation and cleanup, the development watcher,
experimental-native runtime setup, Node/npm compatibility shims, and
Rollup/WebAssembly handling. Gleam build commands also retain a narrow filtered
runner because `shellout` cannot separately capture and filter stderr while
preserving the existing inherited-stream behavior.

## Breaking changes in 6.0.0

`glendix/js/array` now delegates its conversions to `gleam/javascript/array`
Expand Down
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ term_size = ">= 1.0.1 and < 2.0.0"
tom = ">= 2.1.0 and < 3.0.0"
gossamer = ">= 10.0.0 and < 11.0.0"
xmlm = ">= 1.0.1 and < 2.0.0"
shellout = ">= 1.8.0 and < 2.0.0"

[dev_dependencies]
gleeunit = ">= 1.11.0 and < 2.0.0"
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ packages = [
{ name = "plinth", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "D30FDE8FC11EB76538AB498ACFF5399D0AE27993C74D63BE05AFE8CE80C4C839" },
{ name = "redraw", version = "19.2.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "B8CEEB74E8846CE10B8360B924DAD22441B61D947F9449854164F0686C4B8661" },
{ name = "redraw_dom", version = "19.2.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_stdlib", "redraw"], otp_app = "redraw_dom", source = "hex", outer_checksum = "80278296AD6E3D4457D6FF6A14FEA3E90696284BEA297BDE10E430FC4CE726B8" },
{ name = "shellout", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "C416356D45151F298108C9DB9CD1EDE0313F620B5EDBB5766CD7237659D87841" },
{ name = "simplifile", version = "2.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A2727627B063E87351934C7F7F008F2D1FDB16F6DE0B8C79F9E46459CFC9C164" },
{ name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
{ name = "tom", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "DCF04CB7AB35D58CFC598C66EA2E1816D160759802C89B2BA6238780D59BC256" },
Expand All @@ -46,6 +47,7 @@ mendraw = { version = ">= 2.0.0 and < 3.0.0" }
plinth = { version = ">= 0.11.0 and < 1.0.0" }
redraw = { version = ">= 19.2.2 and < 20.0.0" }
redraw_dom = { version = ">= 19.2.2 and < 20.0.0" }
shellout = { version = ">= 1.8.0 and < 2.0.0" }
simplifile = { version = ">= 2.6.0 and < 3.0.0" }
term_size = { version = ">= 1.0.1 and < 2.0.0" }
tom = { version = ">= 2.1.0 and < 3.0.0" }
Expand Down
13 changes: 8 additions & 5 deletions src/glendix/cmd.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import gleam/io
import gleam/list
import gleam/option
import gleam/result
import glendix/command
import glendix/configuration

/// Describes a JavaScript tooling failure.
Expand All @@ -15,8 +16,13 @@ pub type CommandError {

/// Executes a command through the JavaScript process boundary.
pub fn exec(command command: String) -> Result(Nil, CommandError) {
exec_raw(command)
|> map_raw_error("execute command")
command.run(command)
|> result.map_error(fn(error) {
CommandFailed(
operation: "execute command",
reason: command.error_message(error),
)
})
}

/// Detects the configured or lockfile-selected JavaScript package manager.
Expand Down Expand Up @@ -331,9 +337,6 @@ fn map_configuration_error(
}

// -- FFI --
@external(javascript, "./cmd_ffi.mjs", "exec")
fn exec_raw(command command: String) -> Result(Nil, RawCommandError)

@external(javascript, "./cmd_ffi.mjs", "file_exists")
fn file_exists(path: String) -> Bool

Expand Down
63 changes: 4 additions & 59 deletions src/glendix/cmd_ffi.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync, spawnSync } from "node:child_process";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
chmodSync,
Expand All @@ -17,6 +17,7 @@ import { tmpdir } from "node:os";
import { basename, delimiter, dirname, extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { Ok, Error as GleamError } from "../gleam.mjs";
import { runFilteredCommandOrThrow } from "./command_ffi.mjs";

const EXPERIMENTAL_NATIVE_RUNNER = "--glendix-experimental-native-runner";
const EXPERIMENTAL_NATIVE_SHIM = "--glendix-experimental-native-shim";
Expand All @@ -30,62 +31,6 @@ function errorMessage(error) {
export function command_error_message(error) {
return errorMessage(error);
}
function filterErlangWarnings(stderr) {
const lines = stderr.split(/\r?\n/);
const result = [];
let skip = false;
let skipNextEmpty = false;
for (let i = 0; i < lines.length; i++) {
if (!skip && lines[i] === "warning: Unused value") {
if (i + 1 < lines.length && lines[i + 1].includes("gleam_erlang")) {
skip = true;
continue;
}
}
if (skip) {
if (lines[i].includes("not needed")) {
skip = false;
skipNextEmpty = true;
}
continue;
}
if (skipNextEmpty) {
skipNextEmpty = false;
if (lines[i].trim() === "") continue;
}
result.push(lines[i]);
}
return result.join("\n").replace(/\n{3,}/g, "\n\n").trim();
}
function execGleamFiltered(command) {
const result = spawnSync(command, { shell: true, stdio: ["inherit", "pipe", "pipe"] });
if (result.stdout && result.stdout.length > 0) process.stdout.write(result.stdout);
if (result.stderr && result.stderr.length > 0) {
const filtered = filterErlangWarnings(result.stderr.toString());
if (filtered) process.stderr.write(filtered + "\n");
}
if (result.status !== 0) {
const err = new Error("Command failed: " + command);
err.status = result.status;
throw err;
}
}
function execOrThrow(command) {
if (command.startsWith("gleam ")) {
execGleamFiltered(command);
} else {
execSync(command, { stdio: "inherit", shell: true });
}
}

export function exec(command) {
try {
execOrThrow(command);
return new Ok(undefined);
} catch (error) {
return new GleamError(error);
}
}
export function file_exists(path) {
return existsSync(path);
}
Expand Down Expand Up @@ -554,7 +499,7 @@ export function fail_process() {

function setupBridge(bindings) {
generateBindingsOrThrow(bindings);
execGleamFiltered("gleam build");
runFilteredCommandOrThrow("gleam build");
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
const widgetName = pkg.widgetName;
const widgets = pkg.widgets;
Expand Down Expand Up @@ -1174,7 +1119,7 @@ function runDevWithBridgeUsing(execBuild, bindings) {
if (!hasChanges()) return;
console.log("\n[glendix] 변경 감지 → 리빌드");
try {
execGleamFiltered("gleam build");
runFilteredCommandOrThrow("gleam build");
execBuild();
console.log("[glendix] 빌드 완료");
} catch (error) {
Expand Down
101 changes: 101 additions & 0 deletions src/glendix/command.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//// Runs synchronous shell commands while preserving Glendix CLI semantics.
////

import gleam/result
import gleam/string
import shellout

/// Describes a synchronous command execution failure.
@internal
pub type CommandFailure {
/// A command failed with the supplied process-boundary reason.
CommandFailed(command: String, reason: String)
}

/// Runs a shell command synchronously with inherited standard streams.
@internal
pub fn run(command command: String) -> Result(Nil, CommandFailure) {
case string.starts_with(command, "gleam ") {
True -> run_filtered(command)
False -> run_with_shellout(command)
}
}

/// Returns the compatibility error message for a failed command.
@internal
pub fn error_message(failure failure: CommandFailure) -> String {
let CommandFailed(reason:, ..) = failure
reason
}

type RawCommandError

type SigintListeners

fn run_with_shellout(command: String) -> Result(Nil, CommandFailure) {
let listeners = capture_sigint_listeners()
let execution =
shellout.command(
run: shell_executable(),
with: shell_arguments(command),
in: ".",
opt: [shellout.LetBeStdout, shellout.LetBeStderr],
)
restore_sigint_listeners(listeners)
execution
|> result.map(fn(_) { Nil })
|> result.map_error(fn(error) {
let #(_status, output) = error
CommandFailed(
command: command,
reason: shellout_error_message(command, output),
)
})
}

fn shell_executable() -> String {
case is_windows() {
True -> windows_shell()
False -> "/bin/sh"
}
}

fn shell_arguments(command: String) -> List(String) {
case is_windows() {
True -> ["/d", "/s", "/c", command]
False -> ["-c", command]
}
}

fn shellout_error_message(command: String, output: String) -> String {
let summary = "Command failed: " <> command
case string.trim(output) {
"" -> summary
detail -> summary <> "\n" <> detail
}
}

fn run_filtered(command: String) -> Result(Nil, CommandFailure) {
run_filtered_raw(command)
|> result.map_error(fn(error) {
CommandFailed(command: command, reason: raw_error_message(error))
})
}

@external(javascript, "./command_ffi.mjs", "is_windows")
fn is_windows() -> Bool

@external(javascript, "./command_ffi.mjs", "windows_shell")
fn windows_shell() -> String

@external(javascript, "./command_ffi.mjs", "capture_sigint_listeners")
fn capture_sigint_listeners() -> SigintListeners

@external(javascript, "./command_ffi.mjs", "restore_sigint_listeners")
fn restore_sigint_listeners(listeners: SigintListeners) -> Nil

@external(javascript, "./command_ffi.mjs", "run_filtered")
fn run_filtered_raw(command: String) -> Result(Nil, RawCommandError)

@external(javascript, "./command_ffi.mjs", "error_message")
fn raw_error_message(error: RawCommandError) -> String
Loading
Loading