Skip to content

Commit e1ef2e5

Browse files
sapphi-redhi-ogawa
andauthored
feat: plugin legacy (#293)
Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent d9627ee commit e1ef2e5

File tree

10 files changed

+220
-262
lines changed

10 files changed

+220
-262
lines changed

packages/plugin-legacy/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
],
1616
"exports": "./dist/index.js",
1717
"scripts": {
18-
"//dev": "tsdown --watch",
19-
"//build": "tsdown",
18+
"dev": "tsdown --watch",
19+
"build": "tsdown",
2020
"prepublishOnly": "npm run build"
2121
},
2222
"engines": {
@@ -34,6 +34,8 @@
3434
"funding": "https://github.com/vitejs/vite?sponsor=1",
3535
"dependencies": {
3636
"@babel/core": "^7.27.7",
37+
"@babel/plugin-transform-dynamic-import": "^7.27.1",
38+
"@babel/plugin-transform-modules-systemjs": "^7.27.1",
3739
"@babel/preset-env": "^7.27.2",
3840
"browserslist": "^4.25.1",
3941
"browserslist-to-esbuild": "^2.1.1",

packages/plugin-legacy/src/index.ts

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ const _require = createRequire(import.meta.url)
124124
const nonLeadingHashInFileNameRE = /[^/]+\[hash(?::\d+)?\]/
125125
const prefixedHashInFileNameRE = /\W?\[hash(?::\d+)?\]/
126126

127+
const outputOptionsForLegacyChunks =
128+
new WeakSet<Rollup.NormalizedOutputOptions>()
129+
127130
function viteLegacyPlugin(options: Options = {}): Plugin[] {
128131
let config: ResolvedConfig
129132
let targets: Options['targets']
@@ -287,7 +290,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
287290
)
288291
}
289292

290-
if (!isLegacyBundle(bundle, opts)) {
293+
if (!isLegacyBundle(bundle)) {
291294
// Merge discovered modern polyfills to `modernPolyfills`
292295
for (const { modern } of chunkFileNameToPolyfills.values()) {
293296
modern.forEach((p) => modernPolyfills.add(p))
@@ -302,6 +305,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
302305
)
303306
}
304307
await buildPolyfillChunk(
308+
this,
305309
config.mode,
306310
modernPolyfills,
307311
bundle,
@@ -345,6 +349,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
345349
}
346350

347351
await buildPolyfillChunk(
352+
this,
348353
config.mode,
349354
legacyPolyfills,
350355
bundle,
@@ -432,7 +437,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
432437
): Rollup.OutputOptions => {
433438
return {
434439
...options,
435-
format: 'system',
440+
format: 'esm',
436441
entryFileNames: getLegacyOutputFileName(options.entryFileNames),
437442
chunkFileNames: getLegacyOutputFileName(options.chunkFileNames),
438443
}
@@ -451,6 +456,11 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
451456
...(genModern ? [output || {}] : []),
452457
]
453458
}
459+
460+
// @ts-expect-error is readonly but should be injected here
461+
_config.isOutputOptionsForLegacyChunks = (
462+
opts: Rollup.NormalizedOutputOptions,
463+
): boolean => outputOptionsForLegacyChunks.has(opts)
454464
},
455465

456466
async renderChunk(raw, chunk, opts, { chunks }) {
@@ -477,7 +487,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
477487
)
478488
}
479489

480-
if (!isLegacyChunk(chunk, opts)) {
490+
if (!isLegacyChunk(chunk)) {
481491
if (
482492
options.modernPolyfills &&
483493
!Array.isArray(options.modernPolyfills) &&
@@ -526,20 +536,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
526536
return null
527537
}
528538

529-
// @ts-expect-error avoid esbuild transform on legacy chunks since it produces
530-
// legacy-unsafe code - e.g. rewriting object properties into shorthands
531-
opts.__vite_skip_esbuild__ = true
532-
533-
// @ts-expect-error force terser for legacy chunks. This only takes effect if
534-
// minification isn't disabled, because that leaves out the terser plugin
535-
// entirely.
536-
opts.__vite_force_terser__ = true
537-
538-
// @ts-expect-error In the `generateBundle` hook,
539-
// we'll delete the assets from the legacy bundle to avoid emitting duplicate assets.
540-
// But that's still a waste of computing resource.
541-
// So we add this flag to avoid emitting the asset in the first place whenever possible.
542-
opts.__vite_skip_asset_emit__ = true
539+
outputOptionsForLegacyChunks.add(opts)
543540

544541
// avoid emitting assets for legacy bundle
545542
const needPolyfills =
@@ -548,7 +545,23 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
548545
// transform the legacy chunk with @babel/preset-env
549546
const sourceMaps = !!config.build.sourcemap
550547
const babel = await loadBabel()
551-
const result = babel.transform(raw, {
548+
549+
// need to transform into systemjs separately from other plugins
550+
// for preset-env polyfill detection and removal
551+
const resultSystem = babel.transform(raw, {
552+
babelrc: false,
553+
configFile: false,
554+
ast: true,
555+
sourceMaps,
556+
plugins: [
557+
// @ts-expect-error -- not typed
558+
(await import('@babel/plugin-transform-dynamic-import')).default,
559+
// @ts-expect-error -- not typed
560+
(await import('@babel/plugin-transform-modules-systemjs')).default,
561+
],
562+
})
563+
564+
const babelTransformOptions: babel.TransformOptions = {
552565
babelrc: false,
553566
configFile: false,
554567
compact: !!config.build.minify,
@@ -572,8 +585,17 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
572585
createBabelPresetEnvOptions(targets, { needPolyfills }),
573586
],
574587
],
575-
})
576-
588+
}
589+
let result: babel.BabelFileResult | null
590+
if (resultSystem) {
591+
result = babel.transformFromAstSync(
592+
resultSystem.ast!,
593+
resultSystem.code ?? undefined,
594+
babelTransformOptions,
595+
)
596+
} else {
597+
result = babel.transform(raw, babelTransformOptions)
598+
}
577599
if (result) return { code: result.code!, map: result.map }
578600
return null
579601
},
@@ -713,15 +735,19 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
713735
}
714736
},
715737

716-
generateBundle(opts, bundle) {
738+
generateBundle(_opts, bundle) {
717739
if (config.build.ssr) {
718740
return
719741
}
720742

721-
if (isLegacyBundle(bundle, opts) && genModern) {
743+
if (isLegacyBundle(bundle) && genModern) {
722744
// avoid emitting duplicate assets
723745
for (const name in bundle) {
724-
if (bundle[name].type === 'asset' && !/.+\.map$/.test(name)) {
746+
if (
747+
bundle[name].type === 'asset' &&
748+
!/.+\.map$/.test(name) &&
749+
!name.includes('-legacy') // legacy chunks
750+
) {
725751
delete bundle[name]
726752
}
727753
}
@@ -787,6 +813,7 @@ function createBabelPresetEnvOptions(
787813
}
788814

789815
async function buildPolyfillChunk(
816+
ctx: Rollup.PluginContext,
790817
mode: string,
791818
imports: Set<string>,
792819
bundle: Rollup.OutputBundle,
@@ -822,7 +849,7 @@ async function buildPolyfillChunk(
822849
format,
823850
hashCharacters: rollupOutputOptions.hashCharacters,
824851
entryFileNames: rollupOutputOptions.entryFileNames,
825-
sourcemapBaseUrl: rollupOutputOptions.sourcemapBaseUrl,
852+
// sourcemapBaseUrl: rollupOutputOptions.sourcemapBaseUrl,
826853
},
827854
},
828855
},
@@ -855,15 +882,23 @@ async function buildPolyfillChunk(
855882
}
856883

857884
// add the chunk to the bundle
858-
bundle[polyfillChunk.fileName] = polyfillChunk
885+
ctx.emitFile({
886+
type: 'asset',
887+
fileName: polyfillChunk.fileName,
888+
source: polyfillChunk.code,
889+
})
859890
if (polyfillChunk.sourcemapFileName) {
860891
const polyfillChunkMapAsset = _polyfillChunk.output.find(
861892
(chunk) =>
862893
chunk.type === 'asset' &&
863894
chunk.fileName === polyfillChunk.sourcemapFileName,
864895
) as Rollup.OutputAsset | undefined
865896
if (polyfillChunkMapAsset) {
866-
bundle[polyfillChunk.sourcemapFileName] = polyfillChunkMapAsset
897+
ctx.emitFile({
898+
type: 'asset',
899+
fileName: polyfillChunkMapAsset.fileName,
900+
source: polyfillChunkMapAsset.source,
901+
})
867902
}
868903
}
869904
}
@@ -914,26 +949,16 @@ function prependModenChunkLegacyGuardPlugin(): Plugin {
914949
}
915950
}
916951

917-
function isLegacyChunk(
918-
chunk: Rollup.RenderedChunk,
919-
options: Rollup.NormalizedOutputOptions,
920-
) {
921-
return options.format === 'system' && chunk.fileName.includes('-legacy')
952+
function isLegacyChunk(chunk: Rollup.RenderedChunk) {
953+
return chunk.fileName.includes('-legacy')
922954
}
923955

924-
function isLegacyBundle(
925-
bundle: Rollup.OutputBundle,
926-
options: Rollup.NormalizedOutputOptions,
927-
) {
928-
if (options.format === 'system') {
929-
const entryChunk = Object.values(bundle).find(
930-
(output) => output.type === 'chunk' && output.isEntry,
931-
)
932-
933-
return !!entryChunk && entryChunk.fileName.includes('-legacy')
934-
}
956+
function isLegacyBundle(bundle: Rollup.OutputBundle) {
957+
const entryChunk = Object.values(bundle).find(
958+
(output) => output.type === 'chunk' && output.isEntry,
959+
)
935960

936-
return false
961+
return !!entryChunk && entryChunk.fileName.includes('-legacy')
937962
}
938963

939964
function recordAndRemovePolyfillBabelPlugin(

packages/vite/src/node/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import colors from 'picocolors'
1010
import type { Alias, AliasOptions } from 'dep-types/alias'
1111
import picomatch from 'picomatch'
1212
import {
13+
type NormalizedOutputOptions,
1314
type OutputChunk,
1415
type PluginContextMeta,
1516
type RolldownOptions,
@@ -637,6 +638,10 @@ export interface ResolvedConfig
637638
appType: AppType
638639
experimental: RequiredExceptFor<ExperimentalOptions, 'renderBuiltUrl'>
639640
environments: Record<string, ResolvedEnvironmentOptions>
641+
/** @internal injected by legacy plugin */
642+
isOutputOptionsForLegacyChunks?(
643+
outputOptions: NormalizedOutputOptions,
644+
): boolean
640645
/**
641646
* The token to connect to the WebSocket server from browsers.
642647
*

packages/vite/src/node/plugins/css.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
849849
}
850850

851851
if (this.environment.config.build.cssCodeSplit) {
852-
if (opts.format === 'es' || opts.format === 'cjs') {
852+
if (
853+
(opts.format === 'es' || opts.format === 'cjs') &&
854+
!chunk.fileName.includes('-legacy')
855+
) {
853856
const isEntry = chunk.isEntry && isPureCssChunk
854857
const cssFullAssetName = ensureFileExt(chunk.name, '.css')
855858
// if facadeModuleId doesn't exist or doesn't have a CSS extension,
@@ -863,7 +866,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
863866
const originalFileName = getChunkOriginalFileName(
864867
chunk,
865868
config.root,
866-
opts.format,
867869
)
868870

869871
chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName)
@@ -904,20 +906,32 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
904906
`${style}.textContent = ${cssString};` +
905907
`document.head.appendChild(${style});`
906908

907-
// TODO: system js support
908-
// const wrapIdx = code.indexOf('System.register')
909-
// if (wrapIdx >= 0) {
910-
// const executeFnStart = code.indexOf('execute:', wrapIdx)
911-
// injectionPoint = code.indexOf('{', executeFnStart) + 1
912-
// }
913-
const m = (
914-
opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE
915-
).exec(code)
916-
if (!m) {
917-
this.error('Injection point for inlined CSS not found')
909+
let injectionPoint: number
910+
if (opts.format === 'iife' || opts.format === 'umd') {
911+
const m = (
912+
opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE
913+
).exec(code)
914+
if (!m) {
915+
this.error('Injection point for inlined CSS not found')
916+
return
917+
}
918+
injectionPoint = m.index + m[0].length
919+
} else if (opts.format === 'es') {
920+
// legacy build
921+
if (code.startsWith('#!')) {
922+
let secondLinePos = code.indexOf('\n')
923+
if (secondLinePos === -1) {
924+
secondLinePos = 0
925+
}
926+
injectionPoint = secondLinePos
927+
} else {
928+
injectionPoint = 0
929+
}
930+
} else {
931+
this.error('Non supported format')
918932
return
919933
}
920-
const injectionPoint = m.index + m[0].length
934+
921935
s ||= new MagicString(code)
922936
s.appendRight(injectionPoint, injectCode)
923937
}
@@ -954,8 +968,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
954968
},
955969

956970
async generateBundle(opts, bundle) {
957-
// @ts-expect-error asset emits are skipped in legacy bundle
958-
if (opts.__vite_skip_asset_emit__) {
971+
// to avoid emitting duplicate assets for modern build and legacy build
972+
if (this.environment.config.isOutputOptionsForLegacyChunks?.(opts)) {
959973
return
960974
}
961975

packages/vite/src/node/plugins/esbuild.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,9 @@ export const buildEsbuildPlugin = (): Plugin => {
363363
return environment.config.esbuild !== false
364364
},
365365
async renderChunk(code, chunk, opts) {
366-
// @ts-expect-error injected by @vitejs/plugin-legacy
367-
if (opts.__vite_skip_esbuild__) {
366+
// avoid on legacy chunks since it produces legacy-unsafe code
367+
// e.g. rewriting object properties into shorthands
368+
if (this.environment.config.isOutputOptionsForLegacyChunks?.(opts)) {
368369
return null
369370
}
370371

0 commit comments

Comments
 (0)