Rollup-plugin-typescript2: Vue file import

Created on 9 Jan 2019  ·  14Comments  ·  Source: ezolenko/rollup-plugin-typescript2

While switching from rollup-plugin-typescript to rollup-plugin-typescript2 in order to produce declaration files, the *.vue files are not recognized anymore.

[!] (rpt2 plugin) Error: someFolder/index.ts(2,53): semantic error TS2307 Cannot find module './component.vue'.
src\index.ts
Error: someFolder/index.ts(2,53): semantic error TS2307 Cannot find module './component.vue'.

Trying by just importing rollup-plugin-typescript instead of rollup-plugin-typescript2 bundles without a problem.

This could be bound to this issue though I have the last version (of every plugin today indeed).

bug help wanted

Most helpful comment

It works for me with this setup, to compile a single vue component:

Right, but that's not a real use case. That's hello world. For anyone having trouble understanding the problem, here's what I've gleaned.

rollup literally can't do this

why? here's an example:

<script lang="ts">
import Bar from './Bar.vue';
...
</script>

1) vue plugin passes script over to typescript plugin
2) typescript plugin encounters a .vue file, but has no way of knowing what to do with it because rollup doesn't provide a mechanism for plugins to defer to other plugins on imports like webpack. Regular JS can defer to plugins, but code already being processed by a plugin cannot.
3) I actually don't understand why this is different than lang=scss or lang=ts, but it is I guess.

Well, what can I do?

Not much.

But vuetify! buefy!

Vuetify is typescript, but doesn't use SFCs. It's pure render functions.

Buefy uses SFCs and rollup, but no typescript.

Really though, there's nothing I can do?

You aren't going to like it. For every Vue file you need to import from a typescript file, you'll have to create a regular javascript file intermediary.

import Bar from './Bar.vue';

export default Bar;

Then and only then will you be able to build your typescript SFC component lib with rollup.

geez, that sucks

If you've come up with a better solution, I'd love to hear it.

All 14 comments

Could you post your tsconfig and rollup config?

Or a small repo with reproduction :)

I unfortunately don't have a "small" repo. I am working here, trying to migrate from webpack to rollup.

The import in rollup.config.js can be changed to rollup-plugin-typescript2 to see the difference.

Hello.

First of all thank you very much for working on this plugin. It does indeed make much more sense than rollup-plugin-typescript.

I can confirm that this issue exists and setup a small demo repository:
https://github.com/danimoh/rollup-plugin-typescript2-vue-demo

If you commment out the line import AnotherComponent from './AnotherComponent.vue'; it does compile, but unfortunately not with this line enabled.

It's funny that we encountered this issue around the same time. Maybe it was caused by a recent change?
A guess from my side with very limited knowledge about rollup, rollup plugins and typescript would be:
Is it possible that typescript itself is trying to import AnotherComponent instead of rollup or rollup-plugin-vue handling that import first?

That would explain why rollup-plugin-typescript does not have this issue as it compiles on a per file basis with transpileModule.

In this case, the following might be interesting: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#customizing-module-resolution

Any work on this issue is very much appreciated.

Reproduced on both repos, not sure if it is the same problem yet, but it is likely.

To fix second case we do indeed need to send module resolution back to rollup so that vue plugin can do it's thing.

Problem with hooking up rollup's module resolution and typescript is that rollup in later versions returns a Promise from context.resolveId(...). So the call chain looks like this:

  • rollup calls plugin's transform
  • plugin calls typescript's LanguageService.getEmitOutput
  • language service calls plugin's implementation of LanguageHost.resolveModuleNames and expects resolved paths to be returned in that function
  • host calls rollup's PluginContext.resolveId
  • rollup returns a Promise
  • if I understand correctly, all I can do with a promise is to make another promise

LanguageService seems to be strictly synchronous: https://github.com/Microsoft/TypeScript/issues/1857

Plugin.transform can itself return a Promise, but the mechanics of chaining multiple promises made deep inside callbacks on an observer object elude me at the moment.

Hello ezolenko.

Nowadays asynchronous methods in Javascript mostly means methods returning Promises. The new async/await syntax is essentially just syntactic sugar for asynchronous methods that enables the developer to write his code similar to synchronous code, async methods still return promises. await can only be used in async methods though. As you noticed, the LanguageHost.resolveModuleNames method is not asynchronous and therefore it is not possible to return from that method only after waiting for a promise in plain Javascript.

However, in NodeJs, stuff like this is actually possible by yielding the synchronous method on the current thread, then jumping to the async method and jumping back to the synchronous method when the asynchronoues method resolves. See Fibers or wrappers around it like synchronize.js.

Thus, the thing with the async method invocation is not much of a problem actually. There might be another problem though. While the plugin context offers a method resolveId, that's not gonna be enough. We need to call the transform of rollup-plugin-vue to extract the typescript code from the vue single file components. The plugin context does not seem to offer that functionality though unfortunately.

One approach to solve this might be to add rollup-plugin-vue as a dependency to your project and trigger the the transform on the vue plugin directly. That's for sure not beautiful at all and not the intended way to work with rollup plugins.

Another approach might be to run only transpileModule in transform on a per-file basis in a first run to let rollup collect all the imports, let the vue plugin transform the single file components and cache the extracted typescript code. Then before rollup finishes, discard the transpiled code and do a proper typescript compilation on the code we cached in a renderChunk or generateBundle plugin hook. This might interfere with other plugins though that apply additional transformations to the code that we would discard.

For now I don't see a more beautiful solution yet.

Edit: On second thought, the second approach is maybe not _that_ ugly. Instead of renderChunk or generateBundle hooks, the plugin could detect itself when the last import is being imported and at that point start the compilation from scratch and add the compiled file to the rollup queue such that it can actually be processed by all the other plugins also. The previously generated files still need to be discarded though to avoid that they end up in the final bundle.
Still, this approach wastes some processing time as it let's the other plugins also on files we'd discard anyways.

@danimoh @eddow Workaround for both example repos are disabling error checking with // @ts-ignore just above offending imports.

The error is basically typescript complaining that it doesn't know what type *.vue stuff is (Cannot find module means module type). Once that is silenced, everything seems to compile correctly. Downside is that things imported from vue files have any type and don't help with error checking.

(in the minimal repo, first component needs a reference to the second one, otherwise rollup treeshakes it away from the bundle)

@danimoh yep, no way to get module source from rollup via context. Most of it can be done on vue plugin side (I opened a case there), but there are still potential pitfalls, like rpt2 will need to have transformed extracted script before transforming a script that imports it.

I think the approach you describe in the vue issue thread requires typescript to process the files one at a time as it essentially ignores the vue file imports and waits for rollup to feed them back to typescript. Therefore you'd loose type checking across files.

As an alternative to letting the vue plugin handle ts, the following might be a valid approach and a sort of better iteration of the ugly hacks I proposed earlier:

  • Is the vue plugin instance exposed through the options hook?
    That way we could call the transform method on the vue plugin.
  • From https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API it appears to me that the ts CompilerHost and LanguageServiceHost can be fed custom fileExists, readFile, getScriptSnapshot and the like.
  • If both work, we could let typescript parse the code we get from the vue plugin to an AST and collect the imports from that AST and request them from the vue plugin if they are .vue files. For all vue files, we cache the extracted typescript code and overwrite methods like readFile to return that cached ts code for a vue import.

Edit: Actually, if we can overwrite fileExists and readFile, we'd not need to collect imports ourselves by traversing the AST as typescript is just gonna call these methods for all imports it wants to import anyways. We then just need to call the vue plugin on demand.

Vue plugin instance is probably exposed, I don't know if rollup expects plugins to call each other and if something will break (immediately or in the future) in that case.

Your second point will work, that's exactly what LanguageServiceHost is for.

This approach might work, the main downside being potentially fragile coupling with vue plugin and extra cycles for throwaway work.

I wish rollup had a way for plugins to abort transform and declare a dependency to be transformed before current file being retried, then this could be implemented cleanly...

I think there is actually no throwaway work. Typescript will only compile the code once and also the vue plugin will need to process every file only once if we cache the extracted ts code. This approach does not discard any of it's own results or results of other plugins other than my previous suggestion.

Yeah, some architectural change would be required in rollup. Maybe one could open an issue there to implement something like this but it would probably take ages.
Also I'm not sure whether it makes things really much better. We still have to ensure that typescript compiles the whole thing at once to do type checking across all files. Otherwise we might also run into this.

It works for me with this setup, to compile a single vue component:

import VuePlugin from 'rollup-plugin-vue'
import typescript from 'rollup-plugin-typescript2'

export default {
  plugins: [
    typescript({
      typescript: require('typescript'),
      objectHashIgnoreUnknownHack: true,
    }),
    VuePlugin(/* VuePluginOptions */),
  ],
  input: 'src/components/HelloWorld.vue',
  output: [
    { file: 'dist/HelloWorld.cjs.js', format: 'cjs' },
    { file: 'dist/HelloWorld.esm.js', format: 'esm' },
  ],
}

I'm not sure if this is the best way to create a module out of a Vue SFC with lang="ts".

If anyone has any advice, please let me know.

It works for me with this setup, to compile a single vue component:

Right, but that's not a real use case. That's hello world. For anyone having trouble understanding the problem, here's what I've gleaned.

rollup literally can't do this

why? here's an example:

<script lang="ts">
import Bar from './Bar.vue';
...
</script>

1) vue plugin passes script over to typescript plugin
2) typescript plugin encounters a .vue file, but has no way of knowing what to do with it because rollup doesn't provide a mechanism for plugins to defer to other plugins on imports like webpack. Regular JS can defer to plugins, but code already being processed by a plugin cannot.
3) I actually don't understand why this is different than lang=scss or lang=ts, but it is I guess.

Well, what can I do?

Not much.

But vuetify! buefy!

Vuetify is typescript, but doesn't use SFCs. It's pure render functions.

Buefy uses SFCs and rollup, but no typescript.

Really though, there's nothing I can do?

You aren't going to like it. For every Vue file you need to import from a typescript file, you'll have to create a regular javascript file intermediary.

import Bar from './Bar.vue';

export default Bar;

Then and only then will you be able to build your typescript SFC component lib with rollup.

geez, that sucks

If you've come up with a better solution, I'd love to hear it.

Was this page helpful?
0 / 5 - 0 ratings