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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#### :rocket: New Feature

- Rewatch: add `--prod` flag to `build`, `watch`, and `clean` to skip dev-dependencies and dev sources (`"type": "dev"`), enabling builds in environments where dev packages aren't installed (e.g. after `pnpm install --prod`). https://github.com/rescript-lang/rescript/pull/8347
- Rewatch: feature-gated source directories. Tag a source entry with `"feature": "<name>"` and select with `--features a,b` (or per-dep in `dependencies` / `dev-dependencies`) to include optional slices of a package's source tree at build time. Top-level `features` map supports transitive implications. https://github.com/rescript-lang/rescript/pull/8379
- Add `Dict.assignMany`, `Dict.concat`, `Dict.concatMany`, `Dict.concatAll`, `Array.concatAll` to the stdlib. https://github.com/rescript-lang/rescript/pull/8364
- Implement `for...of` and `for await...of` loops. https://github.com/rescript-lang/rescript/pull/7887
- Add support for dict spreads: `dict{...foo, "bar": 2, ...qux}`. https://github.com/rescript-lang/rescript/pull/8369
Expand Down
12 changes: 8 additions & 4 deletions rewatch/CompilerConfigurationSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ This document contains a list of all config parameters with remarks, and whether
| sources | array of Source | | [x] |
| ignored-dirs | array of string | | [_] |
| dependencies | array of string | | [x] |
| dependencies | array of Dependency | See [Features.md](./Features.md). rewatch extension. | [x] |
| dev-dependencies | array of string | | [x] |
| dev-dependencies | array of Dependency | See [Features.md](./Features.md). rewatch extension. | [x] |
| features | map of string to array | See [Features.md](./Features.md). rewatch extension. | [x] |
| generators | array of Rule-Generator | | [_] |
| cut-generators | boolean | | [_] |
| jsx | JSX | | [x] |
Expand All @@ -34,10 +37,11 @@ This document contains a list of all config parameters with remarks, and whether

### Source

| Parameter | JSON type | Remark | Implemented? |
| ---------------- | ------------------------ | ------ | :----------: |
| dir | string | | [x] |
| type | "dev" | | [x] |
| Parameter | JSON type | Remark | Implemented? |
| ---------------- | ------------------------ | ------------------------------------------------------- | :----------: |
| dir | string | | [x] |
| type | "dev" | | [x] |
| feature | string | See [Features.md](./Features.md). rewatch extension. | [x] |
| files | array of string | | [_] |
| files | File-Object | | [_] |
| generators | array of Build-Generator | | [_] |
Expand Down
112 changes: 112 additions & 0 deletions rewatch/Features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Features

Features let a package declare optional parts of its source tree that can be included or excluded at build time. Use them to ship a library with optional backends, experimental modules, or platform-specific code without paying the compile cost when a consumer doesn't need them.

Features are a `rewatch` extension and are not part of the legacy `bsb` build-configuration spec.

## Tagging a source directory

Add a `feature` property to any entry in `sources`. The directory is included in the build only when the feature is active for that package.

```json
{
"name": "@example/lib",
"sources": [
{ "dir": "src" },
{ "dir": "src-native", "feature": "native" },
{ "dir": "src-experimental", "feature": "experimental" }
]
}
```

Untagged source directories (no `feature`) are always compiled. A tagged source's `feature` cascades down into nested `subdirs`: a child that doesn't declare its own `feature` inherits the parent's.

## Declaring feature relationships

A top-level `features` map declares names and optional implications. Listing a feature here is only required when you want one feature to imply another; leaf features can stay undeclared and still work as source-dir tags.

```json
{
"features": {
"full": ["native", "experimental"]
}
}
```

With the above, requesting `full` transitively enables `native` and `experimental`. Cycles (e.g. `a -> b -> a`) are rejected at build time with a clear error.

## Selecting features on the command line

When you run `rewatch build` or `rewatch watch`, pass `--features` to restrict compilation to a specific set. Without the flag, every feature is active and the whole source tree compiles:

```
rewatch build # all features active (default)
rewatch build --features native # only untagged + native
rewatch build --features native,full # multiple features; also expands `full`
```

The CLI flag applies only to the **current package** — the one you're building from. It does not flow down to dependencies; each dependency's active feature set comes from its consumer declarations (see below).

Passing an empty value (`--features ""` or `--features ,`) is rejected. Omit the flag to mean "all features".

## Restricting a dependency's features

When consuming another ReScript package that uses features, switch the entry in `dependencies` or `dev-dependencies` from the shorthand string to an object form and list which features you want:

```json
{
"dependencies": [
"@plain/dep",
{ "name": "@example/lib", "features": ["native"] }
]
}
```

Rules:

- **Shorthand (`"@plain/dep"`)** — the consumer wants every feature of that dependency. This is the existing behavior; nothing changes for configs that don't opt into features.
- **Object with `features`** — the consumer restricts the dependency to the listed features (and whatever they transitively imply through the dependency's own `features` map). An explicit empty list (`"features": []`) means "only untagged source dirs, no feature-gated code".
- **Object without `features`** — equivalent to the shorthand. All features active.

When the same dependency is referenced by multiple consumers with different feature sets, the union of requests wins. If any consumer asks for all features, the dependency builds with all of its features. Features are always additive — enabling more features never removes modules, so the union is always safe.

## Interaction with other flags

- **`type: "dev"` and `--prod`** are orthogonal to features. A source directory may declare both `type: "dev"` and a `feature`; it will only build when both filters pass (not in `--prod`, and the feature is active).
- **`rewatch clean`** ignores `--features` and always cleans the full set of build artifacts across every feature-gated directory. This keeps `clean` predictable regardless of which features happen to be active.

## How incremental builds handle feature changes

Toggling a feature off between builds removes its source files from the build's view. The next `rewatch build` sees the shrunken file set and cleans up the corresponding artifacts (`.mjs`, `.cmj`, etc.) through the same diff mechanism that handles deleted source files.

For `rewatch watch`, a change to `features` in `rescript.json` triggers a full rebuild that recomputes the active set and re-registers watches on the active source directories. The CLI `--features` flag is evaluated once at watcher start; to change it you must restart the watcher.

## Validating feature names

- Unknown feature names in CLI input or source tags are accepted as leaf features (they simply match nothing unless a source directory is tagged with that exact name).
- Cycles in the top-level `features` map are a hard error that names the cycle participants.

## Example

```json
{
"name": "@example/lib",
"sources": [
{ "dir": "src" },
{ "dir": "src-native", "feature": "native" },
{ "dir": "src-web", "feature": "web" },
{ "dir": "src-experimental", "feature": "experimental" }
],
"features": {
"all-backends": ["native", "web"]
},
"dependencies": [
"@plain/dep",
{ "name": "@other/heavy", "features": ["native"] }
]
}
```

- `rewatch build` at `@example/lib` — compiles every source dir; `@other/heavy` builds with just its `native` feature because that's all the consumer requested; `@plain/dep` builds with all of its features.
- `rewatch build --features all-backends` — compiles `src`, `src-native`, `src-web`; skips `src-experimental`.
- `rewatch build --features experimental` — compiles `src`, `src-experimental`; skips the backends.
7 changes: 6 additions & 1 deletion rewatch/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ pub fn get_compiler_info(project_context: &ProjectContext) -> Result<CompilerInf
})
}

#[allow(clippy::too_many_arguments)]
pub fn initialize_build(
default_timing: Option<Duration>,
filter: &Option<regex::Regex>,
Expand All @@ -173,12 +174,13 @@ pub fn initialize_build(
plain_output: bool,
warn_error: Option<String>,
prod: bool,
features: Option<Vec<String>>,
) -> Result<BuildCommandState> {
let project_context = ProjectContext::new(path)?;
let compiler = get_compiler_info(&project_context)?;

let timing_clean_start = Instant::now();
let packages = packages::make(filter, &project_context, show_progress, prod)?;
let packages = packages::make(filter, &project_context, show_progress, prod, features.as_ref())?;

let compiler_check = verify_compiler_info(&packages, &compiler);

Expand All @@ -192,6 +194,7 @@ pub fn initialize_build(
packages,
compiler,
warn_error,
features,
);
packages::parse_packages(&mut build_state)?;

Expand Down Expand Up @@ -589,6 +592,7 @@ pub fn build(
plain_output: bool,
warn_error: Option<String>,
prod: bool,
features: Option<Vec<String>>,
) -> Result<BuildCommandState> {
let default_timing: Option<std::time::Duration> = if no_timing {
Some(std::time::Duration::new(0.0 as u64, 0.0 as u32))
Expand All @@ -604,6 +608,7 @@ pub fn build(
plain_output,
warn_error,
prod,
features,
)
.with_context(|| "Could not initialize build")?;

Expand Down
9 changes: 9 additions & 0 deletions rewatch/src/build/build_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ pub struct BuildCommandState {
pub build_state: BuildState,
// Command-line --warn-error flag override (takes precedence over rescript.json config)
pub warn_error_override: Option<String>,
// Command-line --features override. `None` means all features are active; `Some(list)`
// restricts the root package to those features (and whatever they transitively imply).
pub features: Option<Vec<String>>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -177,18 +180,24 @@ impl BuildCommandState {
packages: AHashMap<String, Package>,
compiler: CompilerInfo,
warn_error_override: Option<String>,
features: Option<Vec<String>>,
) -> Self {
Self {
root_folder,
build_state: BuildState::new(project_context, packages, compiler),
warn_error_override,
features,
}
}

pub fn get_warn_error_override(&self) -> Option<String> {
self.warn_error_override.clone()
}

pub fn get_features(&self) -> Option<Vec<String>> {
self.features.clone()
}

pub fn module_name_package_pairs(&self) -> Vec<(String, String)> {
self.build_state
.modules
Expand Down
5 changes: 4 additions & 1 deletion rewatch/src/build/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@ pub fn cleanup_after_build(build_state: &BuildCommandState) {
pub fn clean(path: &Path, show_progress: bool, plain_output: bool, prod: bool) -> Result<()> {
let project_context = ProjectContext::new(path)?;
let compiler_info = build::get_compiler_info(&project_context)?;
let packages = packages::make(&None, &project_context, show_progress, prod)?;
// `clean` always acts on the full set of source directories regardless of which features are
// active. We explicitly pass `None` so every tagged source folder is included and its
// artifacts can be removed, even for features the user hasn't enabled for this build.
let packages = packages::make(&None, &project_context, show_progress, prod, None)?;

let timing_clean_compiler_assets = Instant::now();
if !plain_output && show_progress {
Expand Down
17 changes: 8 additions & 9 deletions rewatch/src/build/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,10 @@ pub fn compiler_args(
.dependencies
.iter()
.flatten()
.filter_map(|name| pkgs.get(name).map(|pkg| (name.clone(), pkg.path.clone())))
.filter_map(|dep| {
let name = dep.name();
pkgs.get(name).map(|pkg| (name.to_string(), pkg.path.clone()))
})
.collect::<Vec<_>>()
});
resolved.unwrap_or_default()
Expand Down Expand Up @@ -684,20 +687,16 @@ fn get_dependency_paths(
packages: &Option<&AHashMap<String, packages::Package>>,
is_file_type_dev: bool,
) -> Vec<String> {
let normal_deps = config
.dependencies
.clone()
.unwrap_or_default()
let normal_deps: Vec<DependentPackage> = config
.get_dependency_names()
.into_iter()
.map(DependentPackage::Normal)
.collect();

// We can only access dev dependencies for source_files that are marked as "type":"dev"
let dev_deps = if is_file_type_dev {
let dev_deps: Vec<DependentPackage> = if is_file_type_dev {
config
.dev_dependencies
.clone()
.unwrap_or_default()
.get_dev_dependency_names()
.into_iter()
.map(DependentPackage::Dev)
.collect()
Expand Down
9 changes: 3 additions & 6 deletions rewatch/src/build/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ fn get_dep_modules(
// Get the list of allowed dependency packages for this package
let allowed_dependencies: AHashSet<String> = package
.config
.dependencies
.as_ref()
.unwrap_or(&vec![])
.iter()
.chain(package.config.dev_dependencies.as_ref().unwrap_or(&vec![]).iter())
.cloned()
.get_dependency_names()
.into_iter()
.chain(package.config.get_dev_dependency_names())
.collect();

deps.iter()
Expand Down
Loading
Loading