fix(ssrTransform): preserve line offset when transforming imports#19004
fix(ssrTransform): preserve line offset when transforming imports#19004sapphi-red merged 9 commits intovitejs:mainfrom
Conversation
sapphi-red
left a comment
There was a problem hiding this comment.
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.
This comment was marked as outdated.
This comment was marked as outdated.
commit: |
This comment was marked as outdated.
This comment was marked as outdated.
|
The failure of laravel and marko is a known issue on ecosystem-ci side. |
|
@sapphi-red Good call to write that test, as there were bugs 🙈 |
| 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]) | ||
| }) |
There was a problem hiding this comment.
New test is here!
| 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"]});" | ||
| `) | ||
| }) |
There was a problem hiding this comment.
The failing “comment between imports” hoisting test.
| 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"]});" | ||
| `) | ||
| }) |
There was a problem hiding this comment.
Finally, here's a basic “preserve whitespace between imports” hoisting test.
|
/ecosystem-ci run |
|
📝 Ran ecosystem CI on
✅ 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 |
Co-authored-by: 翠 / green <[email protected]>
Co-authored-by: 翠 / green <[email protected]>
|
@hi-ogawa Would you take a look if the coverage change in the Vitest suite in the ecosystem-ci makes sense? |
hi-ogawa
left a comment
There was a problem hiding this comment.
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.
| 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)
…tejs#19004) Co-authored-by: 翠 / green <[email protected]>
Description
The ssrTransformScript function doesn‘t preserve line numbers, and there are two reasons for this:
importstatement being transformedBroken import hoisting
Imports were being hoisted when it wasn't necessary. This was due to a
\ncharacter between imports and the fact thatimportNode.enddoes 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
TODOcomment about this edge case.Preserving line breaks
Often times, an import statement will span multiple lines, particularly when using named exports.
The SSR transform was not respecting this. I've changed the logic to include extra
\ncharacters 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
sourceMappingURLcomments to transformed modules from anode_modulesdirectory, 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.