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 Vue SFC from lang="ts" block #129

Open
eddow opened this issue Jan 9, 2019 · 18 comments
Open

Importing Vue SFC from lang="ts" block #129

eddow opened this issue Jan 9, 2019 · 18 comments
Labels
help wanted problem: removed issue template OP removed the issue template without good cause problem: stale Issue has not been responded to in some time scope: integration Related to an integration, not necessarily to core (but could influence core) scope: vue Related to integration with Vue (rollup-plugin-vue is long archived), not core solution: workaround available There is a workaround available for this issue

Comments

@eddow
Copy link

eddow commented Jan 9, 2019

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).

@ezolenko
Copy link
Owner

ezolenko commented Jan 9, 2019

Could you post your tsconfig and rollup config?

@ezolenko
Copy link
Owner

ezolenko commented Jan 9, 2019

Or a small repo with reproduction :)

@eddow
Copy link
Author

eddow commented Jan 10, 2019

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.

@danimoh
Copy link

danimoh commented Jan 13, 2019

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.

@ezolenko
Copy link
Owner

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: microsoft/TypeScript#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.

@ezolenko
Copy link
Owner

Somewhat related: rollup/rollup#2631

@danimoh
Copy link

danimoh commented Jan 15, 2019

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.

@ezolenko
Copy link
Owner

ezolenko commented Jan 15, 2019

@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)

@ezolenko ezolenko self-assigned this Jan 15, 2019
@ezolenko
Copy link
Owner

@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.

@danimoh
Copy link

danimoh commented Jan 16, 2019

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.

@ezolenko
Copy link
Owner

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...

@danimoh
Copy link

danimoh commented Jan 17, 2019

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.

@ezolenko ezolenko added the kind: bug Something isn't working properly label Feb 20, 2019
@ezolenko ezolenko removed their assignment Mar 13, 2019
@mesqueeb
Copy link
Contributor

mesqueeb commented Jan 20, 2020

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.

@subdavis
Copy link

subdavis commented Oct 9, 2020

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.

@Iskren1990
Copy link

Hi @subdavis, sorry for the question but i am nit that familiar with rollup and its plugins.
Except the Bar.js file that we need to create is there any additional rollup configuration needed ?

@subdavis
Copy link

@Iskren1990 It's been like 18 months since I wrote that, and I don't remember a lot of the context. However, a lot has happened in the world of Vue since Oct. 2020. Most notably, Vite happened.

There's a library mode for this exact thing.

If you're building a component library in 2022, I'd seriously consider using Vite.

@mesqueeb
Copy link
Contributor

I also switched to vite with library mode for bundling. And vue-tsc on the side for generating typescript type files.

@agilgur5 agilgur5 added problem: removed issue template OP removed the issue template without good cause problem: stale Issue has not been responded to in some time solution: workaround available There is a workaround available for this issue scope: integration Related to an integration, not necessarily to core (but could influence core) and removed kind: bug Something isn't working properly labels Apr 24, 2022
@agilgur5 agilgur5 changed the title Vue file import Importing Vue SFC from TS block Jun 1, 2022
@agilgur5
Copy link
Collaborator

agilgur5 commented Sep 11, 2022

Thought I'd note some findings here that I stumbled upon in Webpack-land.

fork-ts-checker-webpack-plugin previously had similar issues with TypeStrong/fork-ts-checker-webpack-plugin#55 + TypeStrong/fork-ts-checker-webpack-plugin#70. These were originally resolved by TypeStrong/fork-ts-checker-webpack-plugin#77, which straight imported vue-parser on this line.

The current version of Vue SFC support in fork-ts-checker-webpack-plugin is also pretty complex and also relies on import/requireing the Vue compiler. But it does so dynamically (similar to how Jest etc read TS files with ts-node, for instance) so that the dependency is optional. (It's also still a significant amount of code, whereas rpt2 itself is intentionally simple at ~1k LoC total -- the Vue SFC integration alone in fork-ts-checker-webpack-plugin is a few hundred LoC at least).

I haven't looked at ts-loader itself, but this shows that even the much larger fork-ts-checker-webpack-plugin couldn't get around this limitation without straight up importing and using the Vue compiler itself 😐 😕 🙃

@agilgur5 agilgur5 added the scope: vue Related to integration with Vue (rollup-plugin-vue is long archived), not core label Mar 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted problem: removed issue template OP removed the issue template without good cause problem: stale Issue has not been responded to in some time scope: integration Related to an integration, not necessarily to core (but could influence core) scope: vue Related to integration with Vue (rollup-plugin-vue is long archived), not core solution: workaround available There is a workaround available for this issue
Projects
None yet
Development

No branches or pull requests

7 participants