I'm having trouble reconciling a custom jsx library and using NodeNext for moduleResolution - tsc complains that tsx-to-html/jsx-runtime cannot be found. The thing - as I understood it - is, that resolving the import of the runtime is neither up to the consumer nor the library... how am I supposed to solve this? Is it simply impossible to use these two options together?
#jsxImportSource and NodeNext
60 messages · Page 1 of 1 (latest)
It's up to tsc resolve the jsx module. What's in your tsconfig.json?
library
{
"compilerOptions": {
"module": "NodeNext",
"target": "ES2022",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": ".",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
}
}
here the error is a bit different, but I guess it's the same reason: "Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './jsx-runtime.js'? [2835]"
The jsxImportSource option should reference the jsx runtime you want to use.
it does - I'm using it from within the library to dogfeed it
with node10 I have no problems, only when I switch to nodeNext
I'm not sure I get what you're doing tbh.
What's the full file path of the tsconfig.json file on your fs? What's the full path to the jsx runtime you're trying to use?
https://gitlab.com/hesxenon/tsx-to-html/-/tree/main if you want you can check it out here - the runtime and the tsconfig are in the same directory and as it stands this works. Changing to NodeNext however breaks the tests and downstream consumers
Is this the tsconfig of the JSX library or the project using the JSX library?
that's the library
for the consumers I can use the bundler resolution... not sure I want that but it's okay for now.
I'm a bit confused, is the library supposed to provide a JSX runtime?
Because the tsconfig says the library is consuming a JSX runtime.
Or is your library internally consumes its own JSX runtime but don't expose it, or it both internally consumes and exposes?
it both internally consumes it (via tests, that's what I meant by dogfeeding) and exposes it to downstream consumers
With jsxImportSource set to . it will generate code import ... from './jsx-runtime'
For whatever reason TS thinks your code is running in ESM (I don't know how your tests are compiled or run since I don't see anything in package.json that indicates that) which means importing without a file extension is illegal.
In general you should separate your package tsconfig from test tsconfig, since they are clearly different. Your package itself doesn't use JSX at all, only the tests do.
ts thinks I'm "running ESM" (resolves modules according to ESM spec) because when I set "moduleResolution" to "NodeNext" that's what it's supposed to do, no?
Anyway, if I do set the module resolution to node next it should not generate import ... from './jsx-runtime' but import ... from './jsx-runtime.js' but apparently this does not happen which is why I currently think this is a bug on TS side
as for the proposed split between the package and test config: for one it doesn't necessarily relate to this problem and on another note I disagree. Diverging configs are always a sign of special treatment and I don't want to think "oh my package works perfectly with jsx" due to a dedicated test config and then realizing later on that consumers have errors because that special treatment didn't make its way downstream.
In this case it might be okay since I'm not shipping the tsconfig but it also doesn't buy me anything
No, Node.js still runs CJS by default to preserve backwards compatibility, it will only run in ESM mode if you have type: module in your package.json, or if you are using .mjs/.mts file extensions, and neither is the case from what I can see.
Node16/NodeNext models this behavior.
But again, I don't see anything that indicates how you are running your tests so I'm not sure how it ends up thinking you are running in ESM.
(As for splitting package tsconfig and tests tsconfig, I very much disagree, the config for writing your own package code is very different from the config your package consumer will use and separating them makes the most sense, you can even test many different tsconfig scenarios. But yeah it's unrelated to the question)
It's not a runtime issue 🙂 TSC itself complains
The "you need file extension for relative import" issue?
Yes that's because Node.js requires you to use file extension when running in ESM, and for whatever reason TS thinks you are running in ESM, it's still the same problem.
whatever reason
I'd think the reason is"moduleResolution": "NodeNext"?
I think something is getting confused here. The repo I linked to compiles as it is now, but if you change the moduleResolution to NodeNext tsc complains that ./jsx-runtime can't be found which tells me it's trying to import the runtime in a non ESM compliant way even though I configured it to resolve modules in an ESM compliant way
No, because of this. It uses CJS unless at least one of those two conditions is met.
So you're saying that when resolving modules - even though I explicitly told typescript to resolve the modules in an ESM compliant way - TSC resolves it according to CJS because of the package json?
I think you are misunderstanding what Node16/NodeNext is supposed to do
Node16/NodeNext means "model it like how Node.js does it" it doesn't mean "model it like ESM"
And the way Node.js does it is like I described above, defaults to CJS unless there are those ESM conditions.
So it's baffling to me why your TS thinks it's in ESM because clearly neither of those conditions is met.
if that's the case it should be easy to fix, I'll get back to you in a minute. For the record though: that's not what I'm reading from the documentation.
To quote:
based on whether Node.js will see an import or require in the output JavaScript code
To me that clearly states that typescript will generateimportwhenmodule: ES*and"./foo.js"whenmoduleResolution: NodeNextor"./foo"whenmoduleResolution: Node10(which is not ESM compliant and that's the whole point about havingNode16/NodeNext)
From allowJs to useDefineForClassFields the TSConfig reference includes information about all of the active compiler flags setting up a TypeScript project.
You can have TS output CJS even when using Node16/NodeNext, it doesn't make it automatically turn into ESM. It turns into ESM when Node.js would turn it into ESM which is what that quoted part means, that's the whole point of Node* (to model it like how Node.js does it)
https://gitlab.com/hesxenon/tsx-to-html/-/tree/nodenext?ref_type=heads
here you can see that even though nodejs is instructed to run ESM typescript still tries to import ./jsx-runtime instead of ./jsx-runtime.js
± % npm run build !10006
> tsx-to-html@0.0.17 build
> tsc
index.test.tsx:7:14 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './jsx-runtime.js'?
7 return <div></div>;
~~~~~~~~~~~
Found 1 error in index.test.tsx:7
as for CJS with NodeNext - TSC doesn't even allow that, you'll have to have module and moduleResolution set to NodeNext
Yeah, that's just how React JSX transform works
That has nothing to do with ESM/CJS, it always imports */jsx-runtime regardless.
The problem is just that in ESM that's not valid without file extension.
exactly. So why does it try to import without extension even though:
- package.json#type: module
- tsconfig#module: NodeNext
- tsconfig#moduleResolution: NodeNext
Because that's the specification of React JSX transform
JSX should get transformed to importing */jsx-runtime, without extension.
which specification are you referring to?
and, if that's the case, that would mean it's impossible (by specification) to create ESM compliant jsx runtimes for node
It is possible, the file extension restriction only applies to relative imports, it doesn't apply to absolute imports like packages.
That's why the new transform is a bit annoying to use if you are consuming the JSX runtime in your own project, and some people set it up to use (IIRC) import map to get around it.
I think @shy parcel has a setup for consuming the library's own JSX runtime somewhere in #random, can't find it right this moment though.
it does, but still */jsx-runtime never refers to a package but a module and thus should have the .js extension imho
hmmm, an importmap 🤔 where would that be set up? tsconfig#paths doesn't work for that, does it? Node loaders are a step too late and <script type="importmap"> has nothing to do with node?
In package.json IIRC.
Never done it myself though.
I just did it via package.json: https://github.com/webstrand/prengine/blob/locus/package.json#L9C3-L9C21
"jsx": "link:jsx",
where jsx is a local folder with its own package.json