IanVS / prettier-plugin-sort-imports

An opinionated but flexible prettier plugin to sort import statements
Apache License 2.0
951 stars 21 forks source link

Vue: Sort both script and setup script #90

Closed IanVS closed 1 year ago

IanVS commented 1 year ago

Fixes https://github.com/trivago/prettier-plugin-sort-imports/issues/218

IanVS commented 1 year ago

Note this also fixes the reproduction in https://github.com/trivago/prettier-plugin-sort-imports/issues/199, since we're not processing the entire contents of the file anymore (I don't think imports can happen outside of a script or setup script block). But I think there's some more we can do to address that issue as well, in a separate PR.

Tanimodori commented 11 months ago

code.replace is not safe to use here.

https://github.com/IanVS/prettier-plugin-sort-imports/blob/c2a67662333de61a23af1fead28067e823eef430/src/preprocessors/vue.ts#L85-L88

The code here is the whole SFC source code. Despite the fact that up to one <script> and one <script setup> block can exist in a single SFC, other 3rd blocks may exist in that SFC (like vue-i18n adds a new <i18n> block). In rare cases String.prototype.replace may replace the wrong place producing tricky issues and it is slower than merging by block offsets in the original fix.

fbartho commented 11 months ago

@Tanimodori that sounds like a serious bug! Would you mind filing a PR to at least give us examples, if not fix it?

I’m not a Vue user, so I only sort of understand your situation here. I’m happy to review any PR that fixes this.

Tanimodori commented 11 months ago

@fbartho Surely. Give me some time to implement that.

Tanimodori commented 11 months ago

@fbartho The fix is blocked by #133. I've opened PR #134 to fix that. But it seems to be blocked by #128 and #129 again...

Tanimodori commented 11 months ago

Let me introduce some backgrounds and give the examples.

Vue Single File Component, or SFC, is a file that contains blocks which describes various aspects of a reusable component similar to Web Component. Here's a typical structure of SFC:

<template>
  <div class="my-text">
    Here's the template block that 
    describes how vue render this component
    into HTML DOM
  </div>
</template>

<script setup lang="ts">
  console.log("Here's the script block that");
  console.log("contains the actual code of this component");
</script>

<style lang="less">
// Here is the style block that
// contains the style used in template
.my-text {
  color: red;
}
</style>

You can tell that different blocks use different languages and the order of blocks does not matter. This prettier plugin only cares about <script> and <script setup> which contains JS/TS code so we use parse from @vue/compiler-sfc to split the blocks.

Tanimodori commented 11 months ago

Here is the problematic example:

<template>
  <code>
import z from 'z';
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from 'vue'
function add(a,b) {
  return a + b;
}

import z from 'z';
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from 'vue'
function add(a,b) {
  return a + b;
}
  </code>
</template>

<script>
import z from 'z';
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from 'vue'
function add(a,b) {
  return a + b;
}
</script>

<script setup>
import z from 'z';
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from 'vue'
function add(a,b) {
  return a + b;
}
</script>

The <template> block contains the same "code text" in <script> and <script setup> here! Well the "code text" in <template> block only render into HTML DOM which does not serve as actual JS/TS code. When we want to show the actual code in a component to demonstrate a specific use case of it in a component library, this example may actually exist. Note that 3rd plugins may also add their custom blocks into SFC.

So now you can tell the problem. As String.prototype.replace only replace the first occurrence and the order of blocks does not matter (some programmers prefer writing <template> first actually), code.replace will replace the content in <template> rather than <script> and <script setup>! Here's the outcome:

<template>
    <code>
        import thirdParty from "third-party"; import { defineComponent } from
        'vue'; import z from 'z'; import otherthing from "@core/otherthing";
        import threeLevelRelativePath from "../../../threeLevelRelativePath";
        import oneLevelRelativePath from "../oneLevelRelativePath"; import
        sameLevelRelativePath from "./sameLevelRelativePath"; function add(a,b)
        { return a + b; } import thirdParty from "third-party"; import {
        defineComponent } from 'vue'; import z from 'z'; import otherthing from
        "@core/otherthing"; import threeLevelRelativePath from
        "../../../threeLevelRelativePath"; import oneLevelRelativePath from
        "../oneLevelRelativePath"; import sameLevelRelativePath from
        "./sameLevelRelativePath"; function add(a,b) { return a + b; }
    </code>
</template>

<script>
import z from "z";
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from "vue";
function add(a, b) {
    return a + b;
}
</script>

<script setup>
import z from "z";
import threeLevelRelativePath from "../../../threeLevelRelativePath";
import sameLevelRelativePath from "./sameLevelRelativePath";
import thirdParty from "third-party";
import oneLevelRelativePath from "../oneLevelRelativePath";
import otherthing from "@core/otherthing";
import { defineComponent } from "vue";
function add(a, b) {
    return a + b;
}
</script>

Although the <code> is compressed a bit due to the HTML Whitespace Sensitivity settings of prettier in this repo's config, you can tell that the wrong place has been replaced.

Tanimodori commented 11 months ago

As the parse tells where a block starts and ends in SFC, the correct and faster way is using that offset to merge blocks like the original fix. The algorithm here is simple here.

  1. Find out the block we want, say that we got one <script> and one <script setup> here. We may have [0,2] blocks actually.
  2. Sort offsets them by the start one, say that we have [b1, e1) and [b2, e2). Blocks may never overlaps.
  3. Format the contents of those blocks, say we got string c1 and c2.
  4. Merge. We use [0, b1) from the original, then c1, then [e1, b2) from the original, then c2 and finally [e2,code.length)
Tanimodori commented 11 months ago

@fbartho I've submitted PR #135 to fix the problem without touching dependencies and shrimpwarp files anyway. Should be able to merge without conflicts.