lukeed / tsm

TypeScript Module Loader
MIT License
1.19k stars 19 forks source link

".mts" and ".cts" default format #4

Closed PabloSzx closed 3 years ago

PabloSzx commented 3 years ago

Shouldn't the config have these defaults to follow the TypeScript logic?

{
  ".mts": {
    "format": "esm"
  },
  ".cts": {
    "format":  "cjs"
  }
}

One could also argue that these extensions format shouldn't even be configurable and force these formats, just as .cjs forces commonjs and .mjs forces ecmascript modules

lukeed commented 3 years ago

It's just invalid TS to have require in a mts file and static import in a cts file. So, like your last paragraph, there's no reason to configure it imo.

Furthered by the fact that these extensions exist so that your typescript can include

import { foobar } from "./hello.mjs";
// Automatically resolved to hello.mts
PabloSzx commented 3 years ago

It's just invalid TS to have require in a mts file and static import in a cts file. So, like your last paragraph, there's no reason to configure it imo.

Furthered by the fact that these extensions exist so that your typescript can include

import { foobar } from "./hello.mjs";
// Automatically resolved to hello.mts

nothing you said changes the fact that tsm should have "format": "cjs" for ".cts" and "format": "esm" for ".mts", which is what I mentioned

lukeed commented 3 years ago

Actually it does, because the integrity of the file itself is determined by the TS checker. tsm transforms the files based on how it was used, so -r loads items as CommonJS and --loader/cli loads it as ESM. So long as the semantics of the files' contents are preserved (they are), then interchanging formats is fine so long as it's consistent.

This is/was a big reason why others like ts-node and even esm are still high friction, because they provide a level of interop up until they don't and then you're stuck in a weird spot.

A real-world example of this is the following:

// src/math.mjs
// or src/math.js w/ type: module
export const sum = (a, b) => a + b;

// test/math.ts
import * as assert from 'assert';
import * as math from '../src/math';

assert.equal(math.sum(1, 2), 3);

run via

$ node -r tsm test/math.ts

This would convert the TS file into require statements, only to load an ESM file that wasn't transformed. Error. Instead, allow the tool to safely translate the contents into their equivalents, as if it were all passing thru a bundler anyway.

The above would work with node --loader tsm test/math.ts or tsm test/math.ts but only because tsm is loaded thru ESM usage and converts the (unaffiliated) TS into ESM, which then handles the source ESM natively. However, working 2/3 times is just added friction when there's no reason to be.

Forcing a format here breaks this and really has no benefit.

PabloSzx commented 3 years ago

your example doesn't have anything to do with what I mentioned, what I am suggesting is following the Typescript 4.5 new convention of .mts is always ESM and .cts is always CJS, that's it, following this convention allows you to specify what is the expected format of the transpilation, since I will know that a typescript file with .cts will have "require" available, while ".mts" will always be esm, that's it

lukeed commented 3 years ago

I understand you, but I don't think you're understanding me.

Forcing a format would break this example. That's because when the src/math.mjs – or src/math.mts for that matter – is loaded, your suggestion would always make it result in ESM syntax (because, natively, it should/is). However, if that were to happen, then the node -r tsm test/math.ts case would have code that looks like this:

// src/math.mjs (converted, forced/remains as ESM)
export const sum = (a, b) => a + b;

// test/math.ts (converted, forced as CJS because of --require hook)
const assert = require('assert');
const math = require('../src/math.mjs');
// ^^ THIS IS STILL ESM -> throws syntax error

Instead, for tsm transpilation, we need to ignore the file's required format and transform it to the usage's desired format. This can only work if the converter itself produces semantically correct format conversion ... and esbuild does.

So instead, when you run node -r tsm test/math.ts on the above example, you should get this:

// src/math.mjs (converted, forced because of --require hook)
const sum = (a, b) => a + b;
exports.sum = sum;

// test/math.ts (converted, forced because of --require hook)
const assert = require('assert');
const math = require('../src/math.mjs');
// ^^ THIS IS NOW COMMONJS -> success

The semantics are preserved. And the same guarantee happens when you run tsm directly or use --loader tsm except everything is coerced into ESM syntax instead.