isaacs / tshy

Other
894 stars 18 forks source link

Feature Request: Support nested conditions #9

Closed Marcel-G closed 1 year ago

Marcel-G commented 1 year ago

Conditional exports can be defined using nested-conditions to specify different entrypoints for browser, node and default for example. It would be nice if these could be expressed using tshy - as far as I can tell this is not yet supported.

Example Config

  "tshy": {
    "exports": {
      ".": {
        "browser": "./src/index.browser.ts",
        "node": "./src/index.node.ts"
      }
    }
  }

Expected Output

  "exports": {
    ".": {
      "browser": {
        "import": {
          "types": "./dist/esm/index.browser.d.ts",
          "default": "./dist/esm/index.browser.js"
        },
        "require": {
          "types": "./dist/commonjs/index.browser.d.ts",
          "default": "./dist/commonjs/index.browser.js"
        }
      },
      "node": {
        "import": {
          "types": "./dist/esm/index.node.d.ts",
          "default": "./dist/esm/index.node.js"
        },
        "require": {
          "types": "./dist/commonjs/index.node.d.ts",
          "default": "./dist/commonjs/index.node.js"
        }
      }
    }

Current Behaviour

tshy.exports . unbuilt exports must not be in ./src, and exports in src must be string values. got: {"browser":"./src/index.browser.ts","node":"./src/index.node.ts"}
isaacs commented 1 year ago

Part of my goal with this is to avoid having to write conditionals in exports as much as possible, in favor of conventions that capture the differences and make it easier for me to know what I'm editing when I'm looking at a filename without having to constantly check with package.json at dev time.

Since you're already using *.browser.js as the filename here, what about just using that as the switch?

Ie, if you have foo.browser.ts or foo.browser.mts, and also a corresponding foo.ts or foo.mts, and src/foo.ts or src/foo.mts is exported, it could just note that there's a browser version, and use that instead?

Then you'd do this:

# files
src/esm-only.mts
src/esm-only.browser.mts
src/index.ts
src/index.browser.ts
{
  "tshy": {
    "exports": {
      "./foo": "./src/esm-only.mts",
      ".": "./src/index.ts"
    },
    "exports": {
      "./foo": {
        "browser": {
          "types": "./dist/esm/esm-only.browser.d.mts",
          "default": "./dist/esm/esm-only.browser.mjs"
        },
        "import": {
          "types": "./dist/esm/esm-only.d.mts",
          "default": "./dist/esm/esm-only.mjs"
        },
      ".": {
        "browser": {
          "types": "./dist/esm/index.browser.d.ts",
          "default": "./dist/esm/index.browser.js"
        },
        "import": {
          "types": "./dist/esm/index.d.ts",
          "default": "./dist/esm/index.js"
        },
        "require": {
          "types": "./dist/commonjs/index.d.ts",
          "default": "./dist/commonjs/index.js"
        }
      }
    }
  }
}

Another alternative would be to treat browser as a dialect, like esm and commonjs, which builds as ESM into ./dist/browser/..., and allows browser-specific overrides named blah-browser.mts just like how blah-cjs.cts overrides blah.ts in the commonjs build. But I think most of the time, the browser and esm builds are pretty much identical, so it's probably better to not have a third entire copy of the lib?

If a fourth "thing" shows up, it's probably better to make it configurable in some way, but I think having browser as a blessed option makes sense. There aren't that many different conditions that people actually use in exports.

Marcel-G commented 1 year ago

Ie, if you have foo.browser.ts or foo.browser.mts, and also a corresponding foo.ts or foo.mts, and src/foo.ts or src/foo.mts is exported, it could just note that there's a browser version, and use that instead?

Thanks for your help, I'll try this out and get back to you.

If a fourth "thing" shows up, it's probably better to make it configurable in some way, but I think having browser as a blessed option makes sense. There aren't that many different conditions that people actually use in exports.

As I understand from the docs the nested conditions allow an extra branching level to be introduced for environment specific entrypoints browser, node and default. It would feel a bit odd to mix module resolution (import/require) with environment branching. However my interpretation could be wrong here

isaacs commented 1 year ago

Hmmmmm.... deno.

Probably it's only a matter of time before there's a bun conditional export, if there isn't already. So maybe this should be a conditional thing? Plus webpack defines a whole bunch of these things

What about something like this?

# files
./src/index.ts
./src/index.deno.ts
./src/index.browser.ts
{
  "tshy": {
    "main": false,
    "exports": { ".": "./src/index.ts" },
    "esmDialects": ["browser", "deno"]
  },
  "exports": {
    ".": {
      "browser": {
        "default": "./dist/esm/index.browser.js",
        "types": "./dist/esm/index.browser.d.ts"
      },
      "deno": {
        "default": "./dist/esm/index.deno.js",
        "types": "./dist/esm/index.deno.d.ts"
      },
      "import": {
        "default": "./dist/esm/index.js",
        "types": "./dist/esm/index.d.ts"
      },
      "require": {
        "default": "./dist/commonjs/index.js",
        "types": "./dist/commonjs/index.d.ts"
      }
    }
  }
}

So the logic is:

isaacs commented 1 year ago

If these were nested under the import and require top-level conditions, it'd be a little cleaner, and also support having a commonjsDialects list as well. Eg:

# files
./src/index.ts
./src/index.browser.ts
./src/index.deno.ts
{
  "tshy": {
    "main": false,
    "exports": { ".": "./src/index.ts" },
    "esmDialects": ["browser", "deno"],
    "commonjsDialects": ["browser"]
  },
  "exports": {
    ".": {
      "import": {
        "browser": {
          "default": "./dist/esm/index.browser.js",
          "types": "./dist/esm/index.browser.d.ts"
        },
        "deno": {
          "default": "./dist/esm/index.deno.js",
          "types": "./dist/esm/index.deno.d.ts"
        },
        "default": "./dist/esm/index.js",
        "types": "./dist/esm/index.d.ts"
      },
      "require": {
        "browser": {
          "types": "./dist/commonjs/index.browser.d.ts",
          "default": "./dist/commonjs/index.browser.js"
        },
        "default": "./dist/commonjs/index.js",
        "types": "./dist/commonjs/index.d.ts"
      }
    }
  }
}
isaacs commented 1 year ago

Ok, not going to get clever with nesting, it makes things too complicated. Just going to make the dialect/override behavior generalized.

2 new optional fields added, commonjsDialects and esmDialects. These will be any arbitrary string, other than 'esm', 'commonjs', 'cjs', 'require', 'import', 'node' or 'default', all for obvious reasons, and must not have any strings in common if both set.

{
  "tshy": {
    "esmDialects": ["deno", "browser"],
    "commonjsDialects": ["whatever"],
  }
}

This will build dist/deno and dist/browser (along with dist/esm) as ESM, and dist/whatever (along with dist/commonjs) as CommonJS.

A file like src/foo-deno.mts will override src/foo.ts in the deno build, and be excluded from all the other builds (same with all the others).