From 1aa434e8017012bf0939b2ff1a3a66b4bd12b76d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 24 Dec 2024 03:19:19 -0500 Subject: [PATCH] fix(ssrTransform): preserve line offset when transforming imports (#19004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- .../node/ssr/__tests__/ssrTransform.spec.ts | 328 ++++++++++-------- packages/vite/src/node/ssr/ssrTransform.ts | 65 ++-- 2 files changed, 222 insertions(+), 171 deletions(-) diff --git a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts index e8570c6d4afedf..d7db971e56218c 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts @@ -14,10 +14,9 @@ const ssrTransformSimpleCode = async (code: string, url?: string) => test('default import', async () => { expect( await ssrTransformSimpleCode(`import foo from 'vue';console.log(foo.bar)`), - ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["default"]}); - console.log(__vite_ssr_import_0__.default.bar)" - `) + ).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["default"]});console.log(__vite_ssr_import_0__.default.bar)"`, + ) }) test('named import', async () => { @@ -25,10 +24,9 @@ test('named import', async () => { await ssrTransformSimpleCode( `import { ref } from 'vue';function foo() { return ref(0) }`, ), - ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["ref"]}); - function foo() { return (0,__vite_ssr_import_0__.ref)(0) }" - `) + ).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["ref"]});function foo() { return (0,__vite_ssr_import_0__.ref)(0) }"`, + ) }) test('named import: arbitrary module namespace specifier', async () => { @@ -36,10 +34,9 @@ test('named import: arbitrary module namespace specifier', async () => { await ssrTransformSimpleCode( `import { "some thing" as ref } from 'vue';function foo() { return ref(0) }`, ), - ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["some thing"]}); - function foo() { return (0,__vite_ssr_import_0__["some thing"])(0) }" - `) + ).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["some thing"]});function foo() { return (0,__vite_ssr_import_0__["some thing"])(0) }"`, + ) }) test('namespace import', async () => { @@ -47,10 +44,9 @@ test('namespace import', async () => { await ssrTransformSimpleCode( `import * as vue from 'vue';function foo() { return vue.ref(0) }`, ), - ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); - function foo() { return __vite_ssr_import_0__.ref(0) }" - `) + ).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");function foo() { return __vite_ssr_import_0__.ref(0) }"`, + ) }) test('export function declaration', async () => { @@ -93,7 +89,6 @@ test('export named from', async () => { await ssrTransformSimpleCode(`export { ref, computed as c } from 'vue'`), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["ref","computed"]}); - Object.defineProperty(__vite_ssr_exports__, "ref", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.ref }}); Object.defineProperty(__vite_ssr_exports__, "c", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.computed }});" `) @@ -106,7 +101,6 @@ test('named exports of imported binding', async () => { ), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["createApp"]}); - Object.defineProperty(__vite_ssr_exports__, "createApp", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }});" `) }) @@ -117,11 +111,9 @@ test('export * from', async () => { `export * from 'vue'\n` + `export * from 'react'`, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); - __vite_ssr_exportAll__(__vite_ssr_import_0__); + "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");__vite_ssr_exportAll__(__vite_ssr_import_0__); ; - const __vite_ssr_import_1__ = await __vite_ssr_import__("react"); - __vite_ssr_exportAll__(__vite_ssr_import_1__); + const __vite_ssr_import_1__ = await __vite_ssr_import__("react");__vite_ssr_exportAll__(__vite_ssr_import_1__); " `) }) @@ -130,7 +122,6 @@ test('export * as from', async () => { expect(await ssrTransformSimpleCode(`export * as foo from 'vue'`)) .toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); - Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});" `) }) @@ -140,7 +131,6 @@ test('export * as from arbitrary module namespace identifier', async () => { await ssrTransformSimpleCode(`export * as "arbitrary string" from 'vue'`), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); - Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});" `) }) @@ -163,7 +153,6 @@ test('export as from arbitrary module namespace identifier', async () => { ), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["arbitrary string2"]}); - Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__["arbitrary string2"] }});" `) }) @@ -180,9 +169,7 @@ test('export then import minified', async () => { `export * from 'vue';import {createApp} from 'vue';`, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["createApp"]}); - const __vite_ssr_import_1__ = await __vite_ssr_import__("vue"); - __vite_ssr_exportAll__(__vite_ssr_import_1__); + "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["createApp"]});const __vite_ssr_import_1__ = await __vite_ssr_import__("vue");__vite_ssr_exportAll__(__vite_ssr_import_1__); " `) }) @@ -192,9 +179,94 @@ test('hoist import to top', async () => { await ssrTransformSimpleCode( `path.resolve('server.js');import path from 'node:path';`, ), + ).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("node:path", {"importedNames":["default"]});__vite_ssr_import_0__.default.resolve('server.js');"`, + ) +}) + +test('whitespace between imports does not trigger hoisting', 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"]});" + `) +}) + +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]) +}) + +// not implemented +test.skip('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"]});" `) }) @@ -220,10 +292,9 @@ test('do not rewrite method definition', async () => { const result = await ssrTransformSimple( `import { fn } from 'vue';class A { fn() { fn() } }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - class A { fn() { (0,__vite_ssr_import_0__.fn)() } }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});class A { fn() { (0,__vite_ssr_import_0__.fn)() } }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -231,10 +302,9 @@ test('do not rewrite when variable is in scope', async () => { const result = await ssrTransformSimple( `import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A(){ const fn = () => {}; return { fn }; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A(){ const fn = () => {}; return { fn }; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -243,10 +313,9 @@ test('do not rewrite when variable is in scope with object destructuring', async const result = await ssrTransformSimple( `import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -255,10 +324,9 @@ test('do not rewrite when variable is in scope with array destructuring', async const result = await ssrTransformSimple( `import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -267,10 +335,9 @@ test('rewrite variable in string interpolation in function nested arguments', as const result = await ssrTransformSimple( `import { fn } from 'vue';function A({foo = \`test\${fn}\`} = {}){ return {}; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A({foo = \`test\${__vite_ssr_import_0__.fn}\`} = {}){ return {}; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A({foo = \`test\${__vite_ssr_import_0__.fn}\`} = {}){ return {}; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -279,10 +346,9 @@ test('rewrite variables in default value of destructuring params', async () => { const result = await ssrTransformSimple( `import { fn } from 'vue';function A({foo = fn}){ return {}; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A({foo = __vite_ssr_import_0__.fn}){ return {}; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A({foo = __vite_ssr_import_0__.fn}){ return {}; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -290,10 +356,9 @@ test('do not rewrite when function declaration is in scope', async () => { const result = await ssrTransformSimple( `import { fn } from 'vue';function A(){ function fn() {}; return { fn }; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]}); - function A(){ function fn() {}; return { fn }; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["fn"]});function A(){ function fn() {}; return { fn }; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -302,10 +367,9 @@ test('do not rewrite when function expression is in scope', async () => { const result = await ssrTransformSimple( `import {fn} from './vue';var a = function() { return function fn() { console.log(fn) } }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]}); - var a = function() { return function fn() { console.log(fn) } }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]});var a = function() { return function fn() { console.log(fn) } }"`, + ) }) // #16452 @@ -313,20 +377,18 @@ test('do not rewrite when function expression is in global scope', async () => { const result = await ssrTransformSimple( `import {fn} from './vue';foo(function fn(a = fn) { console.log(fn) })`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]}); - foo(function fn(a = fn) { console.log(fn) })" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]});foo(function fn(a = fn) { console.log(fn) })"`, + ) }) test('do not rewrite when class declaration is in scope', async () => { const result = await ssrTransformSimple( `import { cls } from 'vue';function A(){ class cls {} return { cls }; }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["cls"]}); - function A(){ class cls {} return { cls }; }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["cls"]});function A(){ class cls {} return { cls }; }"`, + ) expect(result?.deps).toEqual(['vue']) }) @@ -334,30 +396,27 @@ test('do not rewrite when class expression is in scope', async () => { const result = await ssrTransformSimple( `import { cls } from './vue';var a = function() { return class cls { constructor() { console.log(cls) } } }`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]}); - var a = function() { return class cls { constructor() { console.log(cls) } } }" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]});var a = function() { return class cls { constructor() { console.log(cls) } } }"`, + ) }) test('do not rewrite when class expression is in global scope', async () => { const result = await ssrTransformSimple( `import { cls } from './vue';foo(class cls { constructor() { console.log(cls) } })`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]}); - foo(class cls { constructor() { console.log(cls) } })" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]});foo(class cls { constructor() { console.log(cls) } })"`, + ) }) test('do not rewrite catch clause', async () => { const result = await ssrTransformSimple( `import {error} from './dependency';try {} catch(error) {}`, ) - expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["error"]}); - try {} catch(error) {}" - `) + expect(result?.code).toMatchInlineSnapshot( + `"const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["error"]});try {} catch(error) {}"`, + ) expect(result?.deps).toEqual(['./dependency']) }) @@ -368,8 +427,7 @@ test('should declare variable for imported super class', async () => { `import { Foo } from './dependency';` + `class A extends Foo {}`, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]}); - const Foo = __vite_ssr_import_0__.Foo; + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]});const Foo = __vite_ssr_import_0__.Foo; class A extends Foo {}" `) @@ -382,8 +440,7 @@ test('should declare variable for imported super class', async () => { `export class B extends Foo {}`, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]}); - const Foo = __vite_ssr_import_0__.Foo; + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]});const Foo = __vite_ssr_import_0__.Foo; class A extends Foo {}; class B extends Foo {} Object.defineProperty(__vite_ssr_exports__, "B", { enumerable: true, configurable: true, get(){ return B }}); @@ -448,9 +505,7 @@ test('sourcemap is correct for hoisted imports', async () => { const result = (await ssrTransform(code, null, 'input.js', code))! expect(result.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["foo"]}); - const __vite_ssr_import_1__ = await __vite_ssr_import__("vue2", {"importedNames":["bar"]}); - + "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["foo"]});const __vite_ssr_import_1__ = await __vite_ssr_import__("vue2", {"importedNames":["bar"]}); console.log((0,__vite_ssr_import_0__.foo), (0,__vite_ssr_import_1__.bar)); @@ -465,7 +520,7 @@ test('sourcemap is correct for hoisted imports', async () => { column: 0, name: null, }) - expect(originalPositionFor(traceMap, { line: 2, column: 0 })).toStrictEqual({ + expect(originalPositionFor(traceMap, { line: 1, column: 90 })).toStrictEqual({ source: 'input.js', line: 6, column: 0, @@ -532,8 +587,7 @@ test('overwrite bindings', async () => { `function g() { const f = () => { const inject = true }; console.log(inject) }\n`, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["inject"]}); - const a = { inject: __vite_ssr_import_0__.inject }; + "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["inject"]});const a = { inject: __vite_ssr_import_0__.inject }; const b = { test: __vite_ssr_import_0__.inject }; function c() { const { test: inject } = { test: true }; console.log(inject) } const d = __vite_ssr_import_0__.inject; @@ -561,9 +615,8 @@ function c({ _ = bar() + foo() }) {} `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo","bar"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo","bar"]}); const a = ({ _ = (0,__vite_ssr_import_0__.foo)() }) => {}; function b({ _ = (0,__vite_ssr_import_0__.bar)() }) {} function c({ _ = (0,__vite_ssr_import_0__.bar)() + (0,__vite_ssr_import_0__.foo)() }) {} @@ -583,9 +636,8 @@ const a = () => { `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["n"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["n"]}); const a = () => { const { type: n = 'bar' } = {}; console.log(n) @@ -606,9 +658,8 @@ const foo = {} `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["n","m"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["n","m"]}); const foo = {}; { @@ -649,9 +700,8 @@ objRest() `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["remove","add","get","set","rest","objRest"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["remove","add","get","set","rest","objRest"]}); function a() { const { @@ -699,9 +749,8 @@ const obj = { `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]}); const bar = 'bar'; @@ -731,9 +780,8 @@ class A { `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["remove","add"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["remove","add"]}); const add = __vite_ssr_import_0__.add; const remove = __vite_ssr_import_0__.remove; @@ -763,9 +811,8 @@ class A { `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]}); const bar = 'bar'; @@ -809,9 +856,8 @@ bbb() `, ), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["aaa","bbb","ccc","ddd"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["aaa","bbb","ccc","ddd"]}); function foobar() { ddd(); @@ -856,8 +902,6 @@ test('jsx', async () => { .toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("react", {"importedNames":["default"]}); const __vite_ssr_import_1__ = await __vite_ssr_import__("foo", {"importedNames":["Foo","Slot"]}); - - function Bar({ Slot: Slot2 = /* @__PURE__ */ __vite_ssr_import_0__.default.createElement((0,__vite_ssr_import_1__.Foo), null) }) { return /* @__PURE__ */ __vite_ssr_import_0__.default.createElement(__vite_ssr_import_0__.default.Fragment, null, /* @__PURE__ */ __vite_ssr_import_0__.default.createElement(Slot2, null)); } @@ -930,8 +974,7 @@ import foo from "foo"`, ), ).toMatchInlineSnapshot(` "#!/usr/bin/env node - const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]}); - console.log((0,__vite_ssr_import_0__.default)); + const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["default"]});console.log((0,__vite_ssr_import_0__.default)); " `) }) @@ -946,7 +989,6 @@ foo()`, ).toMatchInlineSnapshot(` "#!/usr/bin/env node const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]}); - (0,__vite_ssr_import_0__.foo)()" `) }) @@ -982,7 +1024,6 @@ export class Test { expect(await ssrTransformSimpleCode(code)).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar"]}); - if (false) { const foo = 'foo'; console.log(foo) @@ -1023,9 +1064,8 @@ function test() { return [foo, bar] }`), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar"]}); function test() { if (true) { var foo = () => { var why = 'would' }, bar = 'someone' @@ -1050,9 +1090,8 @@ function test() { return bar; }`), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar","baz"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar","baz"]}); function test() { [__vite_ssr_import_0__.foo]; { @@ -1082,9 +1121,8 @@ for (const test in tests) { console.log(test) }`), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./test.js", {"importedNames":["test"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("./test.js", {"importedNames":["test"]}); for (const test of tests) { console.log(test) @@ -1114,9 +1152,8 @@ const Baz = class extends Foo {} `, ) expect(result?.code).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["default","Bar"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["default","Bar"]}); console.log((0,__vite_ssr_import_0__.default), (0,__vite_ssr_import_0__.Bar)); const obj = { @@ -1135,9 +1172,8 @@ test('import assertion attribute', async () => { import('./bar.json', { with: { type: 'json' } }); `), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo.json"); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo.json"); __vite_ssr_dynamic_import__('./bar.json', { with: { type: 'json' } }); " `) @@ -1157,14 +1193,11 @@ console.log(foo + 2) `), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]}); - console.log(__vite_ssr_import_0__.foo + 1); - const __vite_ssr_import_1__ = await __vite_ssr_import__("./a"); - __vite_ssr_exportAll__(__vite_ssr_import_1__); + const __vite_ssr_import_1__ = await __vite_ssr_import__("./a");__vite_ssr_exportAll__(__vite_ssr_import_1__); ; - const __vite_ssr_import_2__ = await __vite_ssr_import__("./b"); - __vite_ssr_exportAll__(__vite_ssr_import_2__); + const __vite_ssr_import_2__ = await __vite_ssr_import__("./b");__vite_ssr_exportAll__(__vite_ssr_import_2__); ; console.log(__vite_ssr_import_0__.foo + 2) " @@ -1180,12 +1213,10 @@ export * as bar from './bar' console.log(bar) `), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]}); __vite_ssr_exports__.default = (0,__vite_ssr_import_0__.foo)(); const __vite_ssr_import_1__ = await __vite_ssr_import__("./bar"); - Object.defineProperty(__vite_ssr_exports__, "bar", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__ }});; console.log(bar) " @@ -1256,9 +1287,8 @@ switch (1) { } `), ).toMatchInlineSnapshot(` - "const __vite_ssr_import_0__ = await __vite_ssr_import__("./f", {"importedNames":["f"]}); - - + " + const __vite_ssr_import_0__ = await __vite_ssr_import__("./f", {"importedNames":["f"]}); let x = 0; diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index c4552f73229663..1294ece439f1c4 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -126,32 +126,53 @@ async function ssrTransformScript( ) { const source = importNode.source.value as string deps.add(source) - const importId = `__vite_ssr_import_${uid++}__` // Reduce metadata to undefined if it's all default values - if ( - metadata && - (metadata.importedNames == null || metadata.importedNames.length === 0) - ) { - metadata = undefined - } - const metadataStr = metadata ? `, ${JSON.stringify(metadata)}` : '' - - s.update( - importNode.start, - importNode.end, - `const ${importId} = await ${ssrImportKey}(${JSON.stringify( - source, - )}${metadataStr});\n`, - ) + const metadataArg = + (metadata?.importedNames?.length ?? 0) > 0 + ? `, ${JSON.stringify(metadata)}` + : '' - if (importNode.start === index) { - // no need to hoist, but update hoistIndex to keep the order - hoistIndex = importNode.end - } else { - // There will be an error if the module is called before it is imported, - // so the module import statement is hoisted to the top + const importId = `__vite_ssr_import_${uid++}__` + const transformedImport = `const ${importId} = await ${ssrImportKey}(${JSON.stringify( + source, + )}${metadataArg});` + + s.update(importNode.start, importNode.end, transformedImport) + + // If there's only whitespace characters between the last import and the + // current one, that means there's no statements between them and + // hoisting is not needed. + // FIXME: account for comments between imports + const nonWhitespaceRegex = /\S/g + nonWhitespaceRegex.lastIndex = index + nonWhitespaceRegex.exec(code) + if (importNode.start > nonWhitespaceRegex.lastIndex) { + // Imports are moved to the top of the file (AKA “hoisting”) to ensure any + // non-import statements before them are executed after the import. This + // aligns SSR imports with native ESM import behavior. s.move(importNode.start, importNode.end, index) + } else { + // Only update hoistIndex when *not* hoisting the current import. This + // ensures that once any import in this module has been hoisted, all + // remaining imports will also be hoisted. This is inherently true because + // we work from the top of the file downward. + hoistIndex = importNode.end + } + + // Track how many lines the original import statement spans, so we can + // preserve the line offset. + let linesSpanned = 1 + for (let i = importNode.start; i < importNode.end; i++) { + if (code[i] === '\n') { + linesSpanned++ + } + } + if (linesSpanned > 1) { + // This leaves behind any extra newlines that were removed during + // transformation, in the position of the original import statement + // (before any hoisting). + s.prependRight(importNode.end, '\n'.repeat(linesSpanned - 1)) } return importId