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

fix(ssrTransform): preserve line offset when transforming imports #19004

Merged
merged 9 commits into from
Dec 24, 2024

Conversation

aleclarson
Copy link
Member

Description

The ssrTransformScript function doesn‘t preserve line numbers, and there are two reasons for this:

  1. Broken import hoisting
  2. Not preserving the number of line breaks in the import statement being transformed

Broken import hoisting

Imports were being hoisted when it wasn't necessary. This was due to a \n character between imports and the fact that importNode.end does not include the trailing line break.

I've changed this logic to only hoist imports when a non-whitespace character is found between the current import and the previous one. Note that this won't account for JavaScript comments between import statements, but that was already the case. I've added a TODO comment about this edge case.

Preserving line breaks

Often times, an import statement will span multiple lines, particularly when using named exports.

import {
    x,
    y,
    z
} from 'xyz'

The SSR transform was not respecting this. I've changed the logic to include extra \n characters at the end of a transformed import to ensure the line offset is preserved.

Why should line offset be preserved?

The main reason to preserve line offset is it avoids the need for sourcemaps. Vitest prefers not to add sourceMappingURL comments to transformed modules from a node_modules directory, resulting in a poor debugging experience (see vitest-dev/vitest#5605). But it wouldn't be necessary to apply a sourcemap if the line offset was just preserved.

While one could argue that you shouldn't be stepping into an installed package, people do it nonetheless, and it's not a big ask for the experience to not be completely broken by Vite's SSR transform.

Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense.
Would you add a test case for "Broken import hoisting"? It'd be also nice if there's a skipped test case for the edge case related to the comment.

@sapphi-red

This comment was marked as outdated.

Copy link

pkg-pr-new bot commented Dec 19, 2024

Open in Stackblitz

npm i https://pkg.pr.new/vite@19004

commit: 684893d

@vite-ecosystem-ci

This comment was marked as outdated.

@sapphi-red
Copy link
Member

The failure of laravel and marko is a known issue on ecosystem-ci side.
The failure of Vitest is caused by the increase of coverage in this snapshot: https://github.com/vitest-dev/vitest/blob/5ba49475dc1efe10a4d43443f70c683b9fde8720/test/coverage-test/test/vue.test.ts#L30-L45

@aleclarson
Copy link
Member Author

@sapphi-red Good call to write that test, as there were bugs 🙈

Comment on lines +187 to +246
test('preserve line offset when rewriting imports', async () => {
// The line number of each non-import statement must not change.
const inputLines = [
`debugger;`,
``,
`import {`,
` dirname,`,
` join,`,
`} from 'node:path';`,
``,
`debugger;`,
``,
`import fs from 'node:fs';`,
``,
`debugger;`,
``,
`import {`,
` red,`,
` green,`,
`} from 'kleur/colors';`,
``,
`debugger;`,
]

const output = await ssrTransformSimpleCode(inputLines.join('\n'))
expect(output).toBeDefined()

const outputLines = output!.split('\n')
expect(
outputLines
.map((line, i) => `${String(i + 1).padStart(2)} | ${line}`.trimEnd())
.join('\n'),
).toMatchInlineSnapshot(`
" 1 | const __vite_ssr_import_0__ = await __vite_ssr_import__("node:path", {"importedNames":["dirname","join"]});const __vite_ssr_import_1__ = await __vite_ssr_import__("node:fs", {"importedNames":["default"]});const __vite_ssr_import_2__ = await __vite_ssr_import__("kleur/colors", {"importedNames":["red","green"]});debugger;
2 |
3 |
4 |
5 |
6 |
7 |
8 | debugger;
9 |
10 |
11 |
12 | debugger;
13 |
14 |
15 |
16 |
17 |
18 |
19 | debugger;"
`)

// Ensure the debugger statements are still on the same lines.
expect(outputLines[0].endsWith(inputLines[0])).toBe(true)
expect(outputLines[7]).toBe(inputLines[7])
expect(outputLines[11]).toBe(inputLines[11])
expect(outputLines[18]).toBe(inputLines[18])
})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test is here!

Comment on lines 261 to 270
test.fails('comments between imports do not trigger hoisting', async () => {
expect(
await ssrTransformSimpleCode(
`import { dirname } from 'node:path';// comment\nimport fs from 'node:fs';`,
),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("node:path", {"importedNames":["default"]});
__vite_ssr_import_0__.default.resolve('server.js');"
"const __vite_ssr_import_0__ = await __vite_ssr_import__("node:path", {"importedNames":["dirname"]});// comment
const __vite_ssr_import_1__ = await __vite_ssr_import__("node:fs", {"importedNames":["default"]});"
`)
})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failing “comment between imports” hoisting test.

Comment on lines 187 to 198
test('do not hoist import if only whitespace is between them', async () => {
expect(
await ssrTransformSimpleCode(
`import { dirname } from 'node:path';\n\n\nimport fs from 'node:fs';`,
),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("node:path", {"importedNames":["dirname"]});
const __vite_ssr_import_1__ = await __vite_ssr_import__("node:fs", {"importedNames":["default"]});"
`)
})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally, here's a basic “preserve whitespace between imports” hoisting test.

Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests!

packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/ssrTransform.ts Outdated Show resolved Hide resolved
@sapphi-red
Copy link
Member

/ecosystem-ci run

@vite-ecosystem-ci
Copy link

📝 Ran ecosystem CI on 0317abd: Open

suite result latest scheduled
histoire failure failure
laravel failure success
marko failure success
nuxt failure success
redwoodjs failure failure
remix failure failure
vike success failure
vite-plugin-svelte failure failure
vitest failure success
waku failure failure

analogjs, astro, ladle, previewjs, quasar, qwik, rakkas, storybook, sveltekit, unocss, vite-environment-examples, vite-plugin-pwa, vite-plugin-react, vite-plugin-react-swc, vite-plugin-vue, vite-setup-catalogue, vitepress, vuepress

@sapphi-red
Copy link
Member

@hi-ogawa Would you take a look if the coverage change in the Vitest suite in the ecosystem-ci makes sense?

Copy link
Collaborator

@hi-ogawa hi-ogawa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!
I checked Vitest failures in vitest-dev/vitest#7124 and changes are mostly irrelevant for users and actually some bugs are on Vitest side, so it should be fine to proceed with this PR.
I'll follow up with Vitest side fix later to pass ecosystem CI.

@sapphi-red sapphi-red merged commit 1aa434e into vitejs:main Dec 24, 2024
16 checks passed
renovate bot added a commit to andrei-picus-tink/auto-renovate that referenced this pull request Dec 26, 2024
| datasource | package | from  | to    |
| ---------- | ------- | ----- | ----- |
| npm        | vite    | 6.0.5 | 6.0.6 |


## [v6.0.6](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small606-2024-12-26-small)

-   fix: replace runner-side path normalization with `fetchModule`-side resolve ([#18361](vitejs/vite#18361)) ([9f10261](vitejs/vite@9f10261)), closes [#18361](vitejs/vite#18361)
-   fix(css): resolve style tags in HTML files correctly for lightningcss ([#19001](vitejs/vite#19001)) ([afff05c](vitejs/vite@afff05c)), closes [#19001](vitejs/vite#19001)
-   fix(css): show correct error when unknown placeholder is used for CSS modules pattern in lightningcs ([9290d85](vitejs/vite@9290d85)), closes [#19070](vitejs/vite#19070)
-   fix(resolve): handle package.json with UTF-8 BOM ([#19000](vitejs/vite#19000)) ([902567a](vitejs/vite@902567a)), closes [#19000](vitejs/vite#19000)
-   fix(ssrTransform): preserve line offset when transforming imports ([#19004](vitejs/vite#19004)) ([1aa434e](vitejs/vite@1aa434e)), closes [#19004](vitejs/vite#19004)
-   chore: fix typo in comment ([#19067](vitejs/vite#19067)) ([eb06ec3](vitejs/vite@eb06ec3)), closes [#19067](vitejs/vite#19067)
-   chore: update comment about `build.target` ([#19047](vitejs/vite#19047)) ([0e9e81f](vitejs/vite@0e9e81f)), closes [#19047](vitejs/vite#19047)
-   revert: unpin esbuild version ([#19043](vitejs/vite#19043)) ([8bfe247](vitejs/vite@8bfe247)), closes [#19043](vitejs/vite#19043)
-   test(ssr): test virtual module with query ([#19044](vitejs/vite#19044)) ([a1f4b46](vitejs/vite@a1f4b46)), closes [#19044](vitejs/vite#19044)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants