Proposal: Exclude dev sources and dev-dependencies from builds

Problem

rescript.json supports "type": "dev" sources and "dev-dependencies", but the compiler always resolves dev-dependencies in the package tree before building. After pnpm install --prod, the build fails because dev-dependency packages aren’t installed. The --filter flag doesn’t help — package tree resolution happens before source filtering.

Example

// shared/rescript.json
{
  "name": "shared",
  "sources": [
    { "dir": "src", "subdirs": true },
    { "dir": "__tests__", "type": "dev", "subdirs": true }
  ],
  "dev-dependencies": ["rescript-ava"]
}
// app/rescript.json
{
  "name": "app",
  "sources": [{ "dir": "src", "subdirs": true }],
  "dependencies": ["shared"]
}
  • After pnpm install --prod, building fails: upward traversal did not find 'rescript-ava'
  • rescript build --filter '!__tests__' fails with the same error

Suggestion

A flag like rescript build --prod that:

  • Skips "type": "dev" sources (both in the root package and its dependencies)
  • Skips "dev-dependencies" during package tree resolution

Why it would help

  • Production/Docker builds could use pnpm install --prod without breaking the compiler
  • Dev-only packages (testing frameworks, code generators, benchmarking tools, linters, documentation generators, etc.) wouldn’t need to be installed by consumers
  • Monorepos with per-package dev tooling wouldn’t need every dev dependency installed everywhere
  • Smaller attack surface in production — fewer dependencies means fewer potential supply chain vulnerabilities
  • Aligns with how npm/pnpm/yarn distinguish dev vs prod dependencies
3 Likes

Well dev-dependencies of npm dependencies are skipped regardless already. It’s just when they are local or in a monorepo package that rescript will build them. Before we had a dev flag to actually turn them on but building everything and having an explicit opt-out is the more consistent behavior with the JS ecosytem.

1 Like

Ok interesting. I couldn’t quite work out what the current behaviour is.

Are you on ReScript 11? If so, I have exactly the same problem and here’s a workaround.

In package.json:

  "scripts": {
    "postinstall": "make strip_dev_sources"
  },

In Makefile:

.PHONY: strip_dev_sources
strip_dev_sources:
	@node scripts/strip-dev-sources.mjs \
		node_modules/rescript-nodejs/bsconfig.json \
		node_modules/rescript-webapi/bsconfig.json \
		node_modules/@rescript/core/rescript.json \
		node_modules/sury/rescript.json

And the scripts/strip-dev-sources.mjs itself:

#!/usr/bin/env node

/**
 * Strip dev sources from ReScript dependency config files.
 *
 * This script removes all sources with "type": "dev" from bsconfig.json or rescript.json
 * files in node_modules. This is necessary because rewatch --dev true tries to compile
 * ALL dev dependencies across ALL packages, including those that may have missing dev
 * dependencies (like test frameworks).
 *
 * Usage: node scripts/strip-dev-sources.mjs <config-path> [<config-path> ...]
 */

import fs from "fs"
import path from "path"

const configPaths = process.argv.slice(2)

if (configPaths.length === 0) {
  console.error(
    "Usage: strip-dev-sources.mjs <config-path> [<config-path> ...]"
  )
  process.exit(1)
}

let totalStripped = 0

for (const configPath of configPaths) {
  if (!fs.existsSync(configPath)) {
    console.warn(`⚠️  Config file not found: ${configPath}`)
    continue
  }

  try {
    const content = fs.readFileSync(configPath, "utf8")
    const config = JSON.parse(content)

    if (!config.sources || !Array.isArray(config.sources)) {
      continue
    }

    const originalLength = config.sources.length
    config.sources = config.sources.filter((source) => source.type !== "dev")
    const strippedCount = originalLength - config.sources.length

    if (strippedCount > 0) {
      fs.writeFileSync(
        configPath,
        JSON.stringify(config, null, 2) + "\n",
        "utf8"
      )
      console.log(
        `✓ ${path.relative(
          process.cwd(),
          configPath
        )}: stripped ${strippedCount} dev source(s)`
      )
      totalStripped += strippedCount
    }
  } catch (error) {
    console.error(`✗ Error processing ${configPath}: ${error.message}`)
  }
}

if (totalStripped > 0) {
  console.log(
    `\n✓ Total: stripped ${totalStripped} dev source(s) from ${configPaths.length} config(s)`
  )
}

I haven’t migrated to v12 yet, sounds it is solved there.

2 Likes

Thanks for sharing! I’m using ReScript 12. I have work arounds but I’m just proposing that this makes sense to be a feature of the build system.

1 Like