Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: avoid relative paths / file extensions #1

Closed
dreamorosi opened this issue Sep 15, 2023 · 6 comments
Closed

Question: avoid relative paths / file extensions #1

dreamorosi opened this issue Sep 15, 2023 · 6 comments

Comments

@dreamorosi
Copy link

Hi, thanks a lot for creating this package. I saw your tweet yesterday and then another from @atcb here and needless to say I'm very interested.

I'm trying to setup dual bundling on a medium size monorepo and I'm unsure how to go around using relative paths and whether or not the project supports the baseUrl directive in the tsconfig.json file.

I was able to successfully build one of my packages using tshy using file extensions, i.e. given the following three files:

# src/handler.ts
import { fooBar } from './utils/index.js';

fooBar();

# src/utils/index.ts
export { * } from './fooBar.js'

# src/utils/fooBar.ts

export const fooBar = () => true;

However I was wondering if it'd be possible and in the scope of the project to support the usage of baseUrl. Setting baseUrl to './src' would allow me to avoid relative paths altogether and the IDE seems to be fine with that.

I have tried using the property in my tsconfig but I think it's ignored at build time and I the build fails.

Thanks :D

@isaacs
Copy link
Owner

isaacs commented Sep 15, 2023

File extensions are definitely required if you're building for ESM. No way around that. Just put .js on everything.

baseUrl and paths don't actually affect the emitted JavaScript, they just tell TS where to find types. The idea is to help inform tsc and tsserver infer the correct types based on how some later bundler will arrange things.

For example, this builds just fine (fixed the export { * } from which should be export * from)

// ./package.json
{}

// ./tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": true,
    "jsx": "react",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "noUncheckedIndexedAccess": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es2022"
  }
}

// ./src/utils/fooBar.ts
export const fooBar = () => true;

// ./src/utils/index.ts
export * from 'utils/fooBar.js'

// ./src/handler.ts
import { fooBar } from 'utils/index.js';

fooBar();

But then when you try to run it:

$ node dist/commonjs/handler.js
node:internal/modules/cjs/loader:1078
  throw err;
  ^

Error: Cannot find module 'utils/index.js'

Your options:

  1. Learn to love the ..
  2. Put your code in more shallowly nested folders, organize into smaller modules, etc.
  3. Use a post-build bundler that knows how to handle those non-relative paths.
  4. use package exports so that you can import with a local package subpath.

Option (4) is this:

// ./package.json
{
  "name": "@my/package",
  "version": "1.2.3",
  "type": "module",
  "tshy": {
    "exports": {
      "./*": {
        "require": {
          "types": "./dist/commonjs/*.d.ts",
          "default": "./dist/commonjs/*.js"
        },
        "import": {
          "types": "./dist/import/*.d.ts",
          "default": "./dist/import/*.js"
        }
      }
    }
  }
}

// ./tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@my/package/*": [
        "./src/*.js"
      ]
    },
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": true,
    "jsx": "react",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "noUncheckedIndexedAccess": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es2022"
  }
}

// ./src/utils/fooBar.ts
export const fooBar = () => true;

// ./src/utils/index.ts
export * from '@my/package/utils/fooBar'

// ./src/handler.ts
import { fooBar } from '@my/package/utils/index';

fooBar();

But, that only works if the package is linked into a node_modules that node can find. It's not technically an internal package link, because dist/commonjs and dist/esm are "different" packages according to node, since they have a nearer package.json file. This isn't a problem in a workspaces monorepo, because the folder generally will be symlinked into the workspace root's node_modules, but for one-off projects, it'd require an extra step.

A simple solution would be for tshy to symlink the current package into dist/node_modules/<pkgname>. You could also add it to the build step yourself:

{
  "name": "@my/package",
  "scripts": {
    "prepare": "tshy && mkdir -p dist/node_modules/@my && ln -s ../../.. dist/node_modules/@my/package"
  }
}

The main downside with that approach is that all of the code in dist is effectively exported. That might be fine for a standalone app that's a website or something, but it's probably not desirable for library modules.

@isaacs
Copy link
Owner

isaacs commented Sep 15, 2023

Personally, I'm just living with .. and breaking things up into small libs and plugins.

@dreamorosi
Copy link
Author

Hey Isaac, thanks for the exhaustive answer, appreciate you taking the time to explain why it's not possible.

I'm likely going to do the same to be honest, it seems the most straightforward option.

I'll close the issue!

@isaacs
Copy link
Owner

isaacs commented Sep 16, 2023

See also #2, which would leverage node's built-in "imports": {"#blah": ...} style internal modules, which tsc understands in node16/nodenext mode.

@shellscape
Copy link

File extensions are definitely required if you're building for ESM. No way around that. Just put .js on everything.

Does this assume that people using this tool will be writing ESM-first and not CJS-first?

@isaacs
Copy link
Owner

isaacs commented May 3, 2024

Does this assume that people using this tool will be writing ESM-first and not CJS-first?

Neither is "first" to tshy, they're both just dialects. But it is designed with the expectation that you'll be building for both, and writing tests and other random scripts in ESM rather than CommonJS, not for any reason than that's a bit more convenient usually, and what I personally prefer. The only objective benefit really is that ESM can import CommonJS easily and not the other way around, though I also just find it more subjectively appealing.

To be honest, I feel like "there's an extension on my import path" is kind of a weird thing to have an opinion about, if it's just a matter of esthetics. No judgement, but… is it something you could just not care about? Or does it cause some practical issue? If your concern is that you need the .ts paths for a bundler or something, you can always put "moduleResolution": "bundle", "module": "bundle" in your tsconfig.json file, and then tsc won't care one way or the other about your import paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants