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

Importing typescript configured for ESM not supported by Parcel #7823

Closed
itsfarseen opened this issue Mar 13, 2022 · 19 comments
Closed

Importing typescript configured for ESM not supported by Parcel #7823

itsfarseen opened this issue Mar 13, 2022 · 19 comments

Comments

@itsfarseen
Copy link

🐛 bug report

  • Because Typescript developers were super nice, they decided that if you need the generated code to be valid ESM, import "foo" should be written as import "foo.js" even if we are importing foo.ts.
  • Typescript resolution algorithm ignores the extension and imports the .ts file if it's present. Otherwise it tries to import the .js file.
  • Parcel's resolution doesn't follow this convention. If there is no foo.js file but there is foo.ts, Parcel errors out saying foo.js not found.

More info:
image

🎛 Configuration (.babelrc, package.json, cli command)

  • .parcelrc: None
  • Babel: None
  • package.json: { .., "type": "module", .. }

🤔 Expected Behavior

In a TS file, import "foo.js" should try to import foo.ts. If not present, try to import foo.js.

😯 Current Behavior

import "foo.js" fails if foo.js is not present, regardless of the presence of foo.ts

@parcel/core: Failed to resolve '../lib/main.js' from './demo/index.ts'

🔦 Context

Here's my file structure:

lib/
   main.ts
demo/
   index.html
   index.ts

I'm trying to import main.ts in index.ts.
lib is configured to compile to an ESM package, that can work on both client and server side.
So the imports inside main.ts must have .js extension.
Otherwise, typescript will emit it as import "foo", and the import will fail at runtime because ESM specs require an extension.

🧪 Minimal Repro

https://github.com/itsfarseen/repro-parcel-ts

🌍 Your Environment

Software Version(s)
Parcel 2.3.2
Node v14.19.0
Yarn 1.22.17
Operating System Arch Linux
@philkunz
Copy link

A resolution is much needed here. This issue is currently blocking us from migrating our Open Source repos to esm.

@philkunz
Copy link

philkunz commented Mar 15, 2022

Here is note about the reasoning: https://www.typescriptlang.org/docs/handbook/esm-node.html

The TypeScript approach is actually very sensible and also in line with future Types in JavaScript: https://devblogs.microsoft.com/typescript/a-proposal-for-type-syntax-in-javascript/

@itsfarseen
Copy link
Author

Typescript requiring full extension in imports is fine. But I feel like it's typescript's responsibility to rewrite that from ".ts" to ".js". Otherwise, every other tool will need to have workarounds just for the sake of typescript.
Every other JS tool will need to duplicate the logic to "replace .ts with .js", the logic that should have been in the Typescript compiler in the first place.

@philkunz
Copy link

@itsfarseen No absolutely not. An import is an external source. Its part of module resolution to figure out wether it is there in JavaScript or TypeScript format. As to full paths -> that is a requirement of ES modules, so not in TypeScripts scope, especially not with moving to Types out of the box in JavaScript, and supporting TypeScript without actual type checking directly in V8.

@itsfarseen
Copy link
Author

itsfarseen commented Mar 15, 2022

@philkunz Let's consider the pros vs cons. Feel free to add to this list :)

If typescript decides to transpile import statements, just like other language construct it transpiles:

Pros:

  • Bundlers can just look at the imports and invoke tsc/swc/esbuild if it ends in ".ts".
    Otherwise they will have to, for every ".js" imports, check if a ".ts" is available. If yes, try importing that. Otherwise try importing ".js" file.
    Suppose if I build an alternative superset of JS called MangoScript, the bundler will have to look for ".ms" files also.
    This if-else chain increases linearly with the number of languages supported by the bundler.
    So time to import a file = number of languages supported * time to check file exists in the filesystem.
  • Every JS tool just needs to invoke some typescript loader if an import ends in ".ts". This includes things like Jest test runner.
    No need to do the "if .ts exists try loading that" logic.
    No need to add any TS specific logic in the tools other than "invoke TS loader if import ends in .ts".

Cons:

  • Just a little bit of rewrite logic in Typescript.

@philkunz
Copy link

philkunz commented Mar 15, 2022

@itsfarseen It is unnecessary transpilation. TypeScript is a superset of JavaScript. Why transpile something that does not need transpilation? In the future, ideally, you only have to write a .ts file in the module chain, if you want to use a feature, that conflicts with JavaScript syntax. Otherwise, JavaScript is TypeScript and vice versa. You are introducing complexity at the code level, that can be solved in tooling.

Anyway this issue is about getting TypeScript esm support to work in parcel, not about a discussion what TypeScript deems right.

@philkunz
Copy link

philkunz commented Mar 16, 2022

@devongovett @mischnic
Can you tip me off where to look exactly for the resolution stuff? Maybe I can submit a PR then.
Looks like https://github.com/parcel-bundler/parcel/blob/699f0b24c38eabcdad0960c62c03bd2f2902b19e/packages/core/package-manager/src/NodeResolver.js is responsible for the Node stuff. However I'm unsure where to look for the .js not found stuff.

Or would this be solved in swc?

@mischnic
Copy link
Member

No, it's in

const resolver = new NodeResolver({
fs: options.inputFS,
projectRoot: options.projectRoot,
// Extensions are always required in URL dependencies.
extensions:
dependency.specifierType === 'commonjs' ||
dependency.specifierType === 'esm'
? ['ts', 'tsx', 'js', 'jsx', 'json']
: [],
mainFields: ['source', 'browser', 'module', 'main'],
packageManager: options.shouldAutoInstall
? options.packageManager
: undefined,
logger,
});
return resolver.resolve({
filename: specifier,
specifierType: dependency.specifierType,
parent: dependency.resolveFrom,
env: dependency.env,
sourcePath: dependency.sourcePath,
loc: dependency.loc,
});

which calls this class
export default class NodeResolver {

@philkunz
Copy link

philkunz commented Mar 16, 2022

Ah I see.

return resolver.resolve({
filename: specifier,
specifierType: dependency.specifierType,
parent: dependency.resolveFrom,
env: dependency.env,
sourcePath: dependency.sourcePath,
loc: dependency.loc,
});

would basically need to return the .ts file, when no .js is present.
Thank you for the hint.

@smaye81
Copy link

smaye81 commented Jul 11, 2022

Any update on this issue?

@louie-github
Copy link

Another note on the reasoning for this on the TypeScript repo is at TypeScript#27481.

This may be fixed in the future by TypeScript#37582 to allow .ts import paths, however it has been two years since that issue was opened and no progress as of yet has been made. Some of the latest comments seem to be good signs, however.

I think the best approach here is to try to accommodate for this now rather than wait for TypeScript developers to add a flag or configuration option, which could take a long time.

Maybe Parcel could add a config option for resolving .js to .ts when importing from a .ts file if you don't want it to be the default behavior, although you don't really have a choice if you want to use ESM with TypeScript as of now.

I've tried looking through NodeResolver.js but editing the code personally is still a bit daunting for me and I can't figure out where to start or what to modify.

@LiberQuack
Copy link

I implemented a parcel plugin for [dirty] solving that problem

You can check it here
https://github.com/LiberQuack/parcel-resolver-fix-ts-esm-shit

@philkunz
Copy link

I moved on to esbuild. Esbuild just works with esm and is really fast.

@rauschma
Copy link

I’m having the same issue. @LiberQuack’s plugin lets me work around it. Thanks!

@b13nxx
Copy link

b13nxx commented Oct 8, 2022

Current situation with Parcel v2 is

We need to import files using .js extension despite file is index.ts, otherwise TypeScript is complaining:
image

But if we change it to .js extension, then Parcel won't be able to build the project:
image

How can we solve this with Parcel v2 currently? @devongovett @mischnic

@b13nxx
Copy link

b13nxx commented Oct 12, 2022

I have developed a plugin which allows you to use .jsx / .js extensions inside a TypeScript module file. It should also work with all other Parcel specific things (like tilde, absolute chars etc):

parcel-resolver-typescript-esm

@josundt
Copy link

josundt commented Apr 15, 2023

I've made a tiny simple Vanilla (no dependencies) browser app in TypeScript that already runs fine in the browser after compiling TypeScript (to ESM modules).
I use ".js" extensions for all TS module imports according to TypeScript documentation.

I wanted to add a bundler for minification etc, so I decided to try out Parcel for the first time.
When I found that Parcel is not capable of bundling ESM compliant TypeScript source code, I gave up.

@b8kkyn parcel-resolver-typescript-esm did not work according to documentation (with the current version of parcel). For Webpack I use resolve-typescript-plugin which works fine.

I find it a bit strange that native support for ESM compliant TypeScript code is not given priority for bundlers.

@devongovett
Copy link
Member

devongovett commented Oct 15, 2023

Resolving .js to .ts has been supported since Parcel v2.9.0 when we rewrote the resolver. #8807

@github-actions github-actions bot removed the Stale Inactive issues label Oct 15, 2023
@Offirmo
Copy link

Offirmo commented Nov 24, 2023

@devongovett I have the issue, latest Parcel and everything (yarn outdated 100% green):

Screenshot 2023-11-25 at 09 49 18

We can see that Parcel is aware of the correct file ./consts.ts Is there any special configuration to enable?

My config:

{
	"extends": "@parcel/config-default",

	"resolvers": [
		"@parcel/resolver-glob",
		"..."
	]
}

Edit: I made a trivial, 5 lines resolver plugin that remaps .js to .ts and it works 😅

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

No branches or pull requests