Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Remove the legacy build system. Going forward, only the modern build system is supported, and the `rescript-legacy` command is not available anymore. https://github.com/rescript-lang/rescript/pull/8186
- `Int.fromString` and `Float.fromString` use stricter number parsing and no longer uses an explicit radix argument, but instead supports parsing hexadecimal, binary and exponential notation.
- Remove `external-stdlib` configuration option from `rescript.json`. This option was rarely used and is no longer supported.
- `js-post-build` now passes the correct output file path based on `in-source` configuration: when `in-source: true`, the path next to the source file is passed; when `in-source: false`, the path in the `lib/<module>/` directory is passed. Additionally, stdout and stderr from the post-build command are now logged. https://github.com/rescript-lang/rescript/pull/8190

#### :eyeglasses: Spec Compliance

Expand Down
5 changes: 3 additions & 2 deletions docs/docson/build-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@
"type": "object",
"properties": {
"cmd": {
"type": "string"
"type": "string",
"description": "Shell command to run after each JS file is compiled. The absolute path to the output JS file is appended as an argument. The path respects the `in-source` setting."
}
}
},
Expand Down Expand Up @@ -453,7 +454,7 @@
},
"js-post-build": {
"$ref": "#/definitions/js-post-build",
"description": "(Experimental) post-processing hook. The build system will invoke `cmd ${file}` whenever a `${file}` is changed"
"description": "Post-processing hook. The build system will invoke `cmd <absolute-path-to-js-file>` after each JS file is compiled. The path respects the `in-source` setting: when true, the path is next to the source file; when false, the path is in the `lib/<module>/` directory. The command runs with the same working directory as the build process (typically the project root)."
},
"package-specs": {
"$ref": "#/definitions/package-specs",
Expand Down
69 changes: 39 additions & 30 deletions rewatch/CompilerConfigurationSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,35 @@

This document contains a list of all bsconfig parameters with remarks, and whether they are already implemented in rewatch. It is based on https://rescript-lang.org/docs/manual/latest/build-configuration-schema.

| Parameter | JSON type | Remark | Implemented? |
| --------------------- | ----------------------- | ---------------------------------------- | :----------: |
| name | string | | [x] |
| namespace | boolean | | [x] |
| namespace | string | | [x] |
| sources | string | | [x] |
| sources | array of string | | [x] |
| sources | Source | | [x] |
| sources | array of Source | | [x] |
| ignored-dirs | array of string | | [_] |
| dependencies | array of string | | [x] |
| dev-dependencies | array of string | | [x] |
| generators | array of Rule-Generator | | [_] |
| cut-generators | boolean | | [_] |
| jsx | JSX | | [x] |
| gentypeconfig | Gentype | | [x] |
| compiler-flags | array of string | | [x] |
| warnings | Warnings | | [x] |
| ppx-flags | array of string | | [x] |
| pp-flags | array of string | | [_] |
| js-post-build | Js-Post-Build | `${file}` is now an absolute path | [x] |
| package-specs | array of Module-Format | | [_] |
| package-specs | array of Package-Spec | | [x] |
| entries | array of Target-Item | | [_] |
| bs-external-includes | array of string | | [_] |
| suffix | Suffix | | [x] |
| reanalyze | Reanalyze | Reanalyze config; ignored by rewatch | [x] |
| experimental-features | ExperimentalFeatures | | [x] |
| editor | object | VS Code tooling only; ignored by rewatch | [x] |
| Parameter | JSON type | Remark | Implemented? |
| --------------------- | ----------------------- | ----------------------------------------------------------- | :----------: |
| name | string | | [x] |
| namespace | boolean | | [x] |
| namespace | string | | [x] |
| sources | string | | [x] |
| sources | array of string | | [x] |
| sources | Source | | [x] |
| sources | array of Source | | [x] |
| ignored-dirs | array of string | | [_] |
| dependencies | array of string | | [x] |
| dev-dependencies | array of string | | [x] |
| generators | array of Rule-Generator | | [_] |
| cut-generators | boolean | | [_] |
| jsx | JSX | | [x] |
| gentypeconfig | Gentype | | [x] |
| compiler-flags | array of string | | [x] |
| warnings | Warnings | | [x] |
| ppx-flags | array of string | | [x] |
| pp-flags | array of string | | [_] |
| js-post-build | Js-Post-Build | Path respects `in-source` setting; stdout/stderr are logged | [x] |
| package-specs | array of Module-Format | | [_] |
| package-specs | array of Package-Spec | | [x] |
| entries | array of Target-Item | | [_] |
| bs-external-includes | array of string | | [_] |
| suffix | Suffix | | [x] |
| reanalyze | Reanalyze | Reanalyze config; ignored by rewatch | [x] |
| experimental-features | ExperimentalFeatures | | [x] |
| editor | object | VS Code tooling only; ignored by rewatch | [x] |

### Source

Expand Down Expand Up @@ -133,7 +133,16 @@ Currently supported features:

| Parameter | JSON type | Remark | Implemented? |
| --------- | --------- | --------------------------------- | :----------: |
| cmd | string | `${file}` is now an absolute path | [x] |
| cmd | string | Receives absolute path to JS file | [x] |

The path passed to the command respects the `in-source` setting:

- `in-source: true` → path next to the source file (e.g., `src/Foo.js`)
- `in-source: false` → path in `lib/<module>/` directory (e.g., `lib/es6/src/Foo.mjs`)

The command runs with the same working directory as the rewatch process (typically the project root).

stdout and stderr from the command are logged.

### Package-Spec

Expand Down
52 changes: 39 additions & 13 deletions rewatch/src/build/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::project_context::ProjectContext;
use ahash::{AHashMap, AHashSet};
use anyhow::{Result, anyhow};
use console::style;
use log::{debug, trace};
use log::{debug, info, trace, warn};
use rayon::prelude::*;
use std::path::Path;
use std::path::PathBuf;
Expand All @@ -26,25 +26,38 @@ use std::time::SystemTime;
fn execute_post_build_command(cmd: &str, js_file_path: &Path) -> Result<()> {
let full_command = format!("{} {}", cmd, js_file_path.display());

debug!("Executing js-post-build: {}", full_command);

let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(["/C", &full_command]).output()
} else {
Command::new("sh").args(["-c", &full_command]).output()
};

match output {
Ok(output) if !output.status.success() => {
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(anyhow!(
"js-post-build command failed for {}: {}{}",
js_file_path.display(),
stderr,
stdout
))

// Always log stdout/stderr - the user explicitly configured this command
// and likely cares about its output
if !stdout.is_empty() {
info!("{}", stdout.trim());
}
if !stderr.is_empty() {
warn!("{}", stderr.trim());
}

if !output.status.success() {
Err(anyhow!(
"js-post-build command failed for {}",
js_file_path.display()
))
} else {
Ok(())
}
}
Err(e) => Err(anyhow!("Failed to execute js-post-build command: {}", e)),
Ok(_) => Ok(()),
}
}

Expand Down Expand Up @@ -853,10 +866,23 @@ fn compile_file(
{
// Execute post-build command for each package spec (each output format)
for spec in root_config.get_package_specs() {
let js_file = helpers::get_source_file_from_rescript_file(
&package.get_build_path().join(path),
&root_config.get_suffix(&spec),
);
// Determine the correct JS file path based on in-source setting:
// - in-source: true -> next to the source file (e.g., src/Foo.js)
// - in-source: false -> in lib/<module>/ directory (e.g., lib/es6/src/Foo.js)
let js_file = if spec.in_source {
helpers::get_source_file_from_rescript_file(
&Path::new(&package.path).join(path),
&root_config.get_suffix(&spec),
)
} else {
helpers::get_source_file_from_rescript_file(
&Path::new(&package.path)
.join("lib")
.join(spec.get_out_of_source_dir())
.join(path),
&root_config.get_suffix(&spec),
)
};

if js_file.exists() {
// Fail the build if post-build command fails (matches bsb behavior with &&)
Expand Down
51 changes: 51 additions & 0 deletions tests/build_tests/post-build-out-of-source/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @ts-check

import * as assert from "node:assert";
import * as fs from "node:fs";
import * as path from "node:path";
import { setup } from "#dev/process";

const { execBuild, execClean } = setup(import.meta.dirname);

const isWindows = process.platform === "win32";

const logFile = path.join(import.meta.dirname, "post-build-paths.txt");

// Clean up any previous log file
if (fs.existsSync(logFile)) {
fs.unlinkSync(logFile);
}

const out = await execBuild();

if (out.status !== 0) {
assert.fail(out.stdout + out.stderr);
}

try {
// Verify that the post-build command received the correct paths
assert.ok(fs.existsSync(logFile), "post-build-paths.txt should exist");

const paths = fs.readFileSync(logFile, "utf-8").trim().split("\n");

// With in-source: false and module: esmodule, the paths should be in lib/es6/
// e.g., /path/to/post-build-out-of-source/lib/es6/src/demo.mjs (Unix)
// e.g., C:\path\to\post-build-out-of-source\lib\es6\src\demo.mjs (Windows)
const libEs6Sep = isWindows ? "\\lib\\es6\\" : "/lib/es6/";
const libBsSep = isWindows ? "\\lib\\bs\\" : "/lib/bs/";

for (const p of paths) {
assert.ok(
p.includes(libEs6Sep) && p.endsWith(".mjs"),
`Path should be in lib/es6/ directory with .mjs suffix: ${p}`,
);
// Should NOT be in lib/bs/ when in-source is false
assert.ok(!p.includes(libBsSep), `Path should not be in lib/bs/: ${p}`);
}
} finally {
// Clean up log file
if (fs.existsSync(logFile)) {
fs.unlinkSync(logFile);
}
await execClean();
}
6 changes: 6 additions & 0 deletions tests/build_tests/post-build-out-of-source/log-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as fs from "node:fs";
import * as path from "node:path";

const jsFile = process.argv[2];
const logFile = path.join(import.meta.dirname, "post-build-paths.txt");
fs.appendFileSync(logFile, jsFile + "\n");
16 changes: 16 additions & 0 deletions tests/build_tests/post-build-out-of-source/rescript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "post-build-out-of-source",
"sources": {
"dir": "src",
"subdirs": true
},
"package-specs": {
"module": "esmodule",
"in-source": false
},
"suffix": ".mjs",
"js-post-build": { "cmd": "node ./log-path.js" },
"warnings": {
"error": "+101"
}
}
1 change: 1 addition & 0 deletions tests/build_tests/post-build-out-of-source/src/demo.res
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let x = 1
44 changes: 40 additions & 4 deletions tests/build_tests/post-build/input.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
// @ts-check

import * as assert from "node:assert";
import * as fs from "node:fs";
import * as path from "node:path";
import { setup } from "#dev/process";

const { execBuild, execClean } = setup(import.meta.dirname);

if (process.platform === "win32") {
console.log("Skipping test on Windows");
process.exit(0);
const isWindows = process.platform === "win32";

// Use a node script for cross-platform path logging
const logFile = path.join(import.meta.dirname, "post-build-paths.txt");

// Clean up any previous log file
if (fs.existsSync(logFile)) {
fs.unlinkSync(logFile);
}

const out = await execBuild();
Expand All @@ -16,4 +23,33 @@ if (out.status !== 0) {
assert.fail(out.stdout + out.stderr);
}

await execClean();
try {
// Verify that the post-build command received the correct paths
assert.ok(fs.existsSync(logFile), "post-build-paths.txt should exist");

const paths = fs.readFileSync(logFile, "utf-8").trim().split("\n");

// With in-source: true, the paths should be next to the source files
// e.g., /path/to/post-build/src/demo.js (Unix)
// e.g., C:\path\to\post-build\src\demo.js (Windows)
const srcSep = isWindows ? "\\src\\" : "/src/";
const libBsSep = isWindows ? "\\lib\\bs\\" : "/lib/bs/";

for (const p of paths) {
assert.ok(
p.includes(srcSep) && p.endsWith(".js"),
`Path should be in src/ directory: ${p}`,
);
// Should NOT be in lib/bs/ when in-source is true
assert.ok(
!p.includes(libBsSep),
`Path should not be in lib/bs/ when in-source is true: ${p}`,
);
}
} finally {
// Clean up log file
if (fs.existsSync(logFile)) {
fs.unlinkSync(logFile);
}
await execClean();
}
6 changes: 6 additions & 0 deletions tests/build_tests/post-build/log-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as fs from "node:fs";
import * as path from "node:path";

const jsFile = process.argv[2];
const logFile = path.join(import.meta.dirname, "post-build-paths.txt");
fs.appendFileSync(logFile, jsFile + "\n");
6 changes: 5 additions & 1 deletion tests/build_tests/post-build/rescript.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
"dir": "src",
"subdirs": true
},
"js-post-build": { "cmd": "cat" },
"package-specs": {
"module": "commonjs",
"in-source": true
},
"js-post-build": { "cmd": "node ./log-path.js" },
"warnings": {
"error": "+101"
}
Expand Down
2 changes: 1 addition & 1 deletion tests/build_tests/post-build/src/demo.res
Original file line number Diff line number Diff line change
@@ -1 +1 @@
let () = Js.log("Hello, ReScript")
let () = Console.log("Hello, ReScript")
Loading