Skip to content

Commit ec17b8f

Browse files
poc: migrate from Vite 5 to Vite 8 (Rolldown)
This is a proof-of-concept for upgrading the marketplace webapp from Vite 5 (esbuild) to Vite 8 (Rolldown-based). The migration surfaces several compatibility issues that required workarounds. Changes: - Upgrade Vite to v8 with @rolldown/plugin-node-polyfills replacing the previous esbuild-based polyfill plugins - Switch from @vitejs/plugin-react-swc to @vitejs/plugin-react - Add Vite plugins to work around Rolldown CJS/ESM interop issues: - cjsNamedImportsFix: rewrites named imports from CJS deep paths (dcl-catalyst-client/dist/*, ethers/lib/utils) to default import + property access, since Rolldown cannot extract named exports from CJS - cjsDefaultImportFix: fixes default imports from CJS packages where the wrapper object is returned instead of .default (react-countup) - fixDecentralandTransactionsTDZ: defers the contracts object in decentraland-transactions to a lazy initializer, preventing TDZ errors when ContractName is used as computed keys before the enum module finishes evaluating - cjs-toesm-fix: patches Rolldown's __toESM helper to not skip the __esModule check - Break circular dependency in vendor/types.ts by importing ContractName directly from ./decentraland/contracts instead of the barrel ./decentraland/index.ts (cycle: vendor/types -> vendor/decentraland -> BidService -> vendor/types caused ContractName TDZ error) - Move theme CSS imports to the top of src/index.tsx so Semantic UI base styles load before component-level dark theme overrides (in Vite dev mode each CSS file is a separate <style> tag, so cascade order depends on import order — unlike production where everything is bundled into one file) - Update decentraland-ui import paths from lib/ to dist/ - Use esbuild for CSS minification to avoid Lightning CSS errors with invalid pseudo-element selectors in Semantic UI - Bundle local decentraland-ui tgz for CI compatibility
1 parent 8017b27 commit ec17b8f

18 files changed

Lines changed: 1611 additions & 1488 deletions

File tree

webapp/.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
scripts/
2+
vite.config.ts

webapp/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ yarn-error.log*
2626

2727
# Sentry Auth Token
2828
.sentryclirc
29+
30+
31+
# MCP
32+
.playwright-mcp
58.9 MB
Binary file not shown.

webapp/package-lock.json

Lines changed: 1160 additions & 1429 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webapp/package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@0xsquid/squid-types": "^0.1.78",
88
"@covalenthq/client-sdk": "^0.6.4",
99
"@dcl/crypto": "^3.0.0",
10+
"@dcl/hooks": "^1.3.0",
1011
"@dcl/schemas": "^22.1.0",
1112
"@dcl/single-sign-on-client": "^0.1.0",
1213
"@dcl/ui-env": "^2.0.0",
@@ -23,7 +24,7 @@
2324
"decentraland-crypto-fetch": "^2.0.1",
2425
"decentraland-dapps": "^28.0.1",
2526
"decentraland-transactions": "^3.0.2",
26-
"decentraland-ui": "^7.1.0",
27+
"decentraland-ui": "file:decentraland-ui-0.0.0-development.tgz",
2728
"decentraland-ui2": "^1.3.7",
2829
"ethers": "^5.6.8",
2930
"graphql": "^14.7.0",
@@ -34,6 +35,7 @@
3435
"react-countup": "^6.2.0",
3536
"react-dom": "^18.3.1",
3637
"react-intersection-observer": "^9.4.3",
38+
"react-intl": "^10.1.1",
3739
"react-lazy-load-image-component": "^1.5.6",
3840
"react-redux": "^7.2.4",
3941
"react-router-dom": "^5.3.4",
@@ -51,8 +53,7 @@
5153
},
5254
"devDependencies": {
5355
"@dcl/eslint-config": "^2.0.0",
54-
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
55-
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
56+
"@rolldown/plugin-node-polyfills": "^1.0.3",
5657
"@sentry/cli": "^2.20.5",
5758
"@swc/core": "^1.3.104",
5859
"@swc/jest": "^0.2.30",
@@ -78,11 +79,12 @@
7879
"@types/redux-logger": "3.0.6",
7980
"@typescript-eslint/eslint-plugin": "^7.1.1",
8081
"@typescript-eslint/parser": "^7.1.1",
81-
"@vitejs/plugin-react-swc": "^3.8.0",
82+
"@vitejs/plugin-react": "^6.0.0",
8283
"css-mediaquery": "^0.1.2",
8384
"customize-cra": "^1.0.0",
8485
"dcl-tslint-config-standard": "^3.0.0",
8586
"dotenv": "^16.3.1",
87+
"esbuild": "^0.27.4",
8688
"eslint": "^8.57.0",
8789
"eslint-plugin-react-hooks": "^4.6.0",
8890
"eslint-plugin-react-refresh": "^0.4.5",
@@ -95,16 +97,15 @@
9597
"node-fetch": "^2.7.0",
9698
"prettier": "^3.2.5",
9799
"redux-saga-test-plan": "^4.0.3",
98-
"rollup-plugin-polyfill-node": "^0.13.0",
99100
"ts-node": "^10.9.2",
100101
"tslint": "^5.20.1",
101102
"tslint-react": "^4.1.0",
102103
"typechain": "^8.0.0",
103-
"typescript": "5.4",
104-
"vite": "^6.2.0"
104+
"typescript": "~5.7.2",
105+
"vite": "^8.0.0"
105106
},
106107
"scripts": {
107-
"start": "vite",
108+
"start": "vite optimize && node scripts/patch-deps.cjs && vite",
108109
"preview": "vite preview",
109110
"prebuild": "node scripts/prebuild.cjs",
110111
"build": "tsc && node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
@@ -129,4 +130,4 @@
129130
"type": "git",
130131
"url": "https://github.com/decentraland/marketplace.git"
131132
}
132-
}
133+
}

webapp/scripts/patch-deps.cjs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* Workaround for two Rolldown CJS interop issues in Vite 8 pre-bundled deps:
3+
*
4+
* 1. __toESM isNodeMode: Rolldown passes isNodeMode=1 to __toESM which skips
5+
* the __esModule check, double-wrapping .default on CJS modules that already
6+
* set __esModule: true.
7+
*
8+
* 2. Missing named exports: Rolldown wraps CJS barrel exports (like decentraland-ui)
9+
* as a single default export without generating individual ESM named exports.
10+
* This breaks `import { Foo } from 'cjs-package'` patterns.
11+
*
12+
* Run after `vite optimize` and before `vite` to fix the cached deps.
13+
*/
14+
const fs = require('fs')
15+
const path = require('path')
16+
17+
const depsDir = path.join(__dirname, '..', 'node_modules', '.vite', 'deps')
18+
19+
/**
20+
* Patch 1: Fix __toESM isNodeMode to always respect __esModule flag.
21+
*/
22+
function patchToESM(files) {
23+
let count = 0
24+
for (const file of files) {
25+
const filePath = path.join(depsDir, file)
26+
const content = fs.readFileSync(filePath, 'utf-8')
27+
if (!content.includes('var __toESM')) continue
28+
const patched = content.replace(
29+
/var __toESM = \(mod, isNodeMode, target\) => \(target = mod != null \? __create\(__getProtoOf\(mod\)\) : \{\}, __copyProps\(isNodeMode \|\| !mod \|\| !mod\.__esModule/,
30+
'var __toESM = (mod, _isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(!mod || !mod.__esModule'
31+
)
32+
if (patched !== content) {
33+
fs.writeFileSync(filePath, patched)
34+
count++
35+
console.log(` Patched __toESM in ${file}`)
36+
}
37+
}
38+
return count
39+
}
40+
41+
/**
42+
* Extract named export keys from bundled CJS code when we can't require() the package.
43+
* Looks for patterns like `exports.Foo = ...` and `exports["Foo"] = ...` inside
44+
* __commonJSMin wrappers. Also follows `__exportStar(require_xxx(), exports)` calls
45+
* to find re-exported names from sub-modules within the same chunk.
46+
*/
47+
function extractExportNames(content) {
48+
const names = new Set()
49+
// Match: exports.Name = ... (but not exports.__esModule)
50+
const dotPattern = /exports\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g
51+
let m
52+
while ((m = dotPattern.exec(content)) !== null) {
53+
const name = m[1]
54+
if (name !== '__esModule' && name !== 'default') {
55+
names.add(name)
56+
}
57+
}
58+
// Match: exports["Name"] = ...
59+
const bracketPattern = /exports\["([a-zA-Z_$][a-zA-Z0-9_$]*)"\]\s*=/g
60+
while ((m = bracketPattern.exec(content)) !== null) {
61+
const name = m[1]
62+
if (name !== '__esModule' && name !== 'default') {
63+
names.add(name)
64+
}
65+
}
66+
67+
// Follow __exportStar references: __exportStar(require_xxx(), exports)
68+
// These copy all properties from a sub-module. Find the sub-module's exports
69+
// by looking at the referenced require_xxx function's body.
70+
const exportStarPattern = /__exportStar\((require_\w+)\(\)/g
71+
while ((m = exportStarPattern.exec(content)) !== null) {
72+
const fnName = m[1]
73+
// Find the function body: var require_xxx = __commonJSMin(((exports) => { ... }))
74+
const fnDefRe = new RegExp(`var ${fnName.replace(/\$/g, '\\$')} = /\\* @__PURE__ \\*/ __commonJSMin\\(\\(\\(exports\\) => \\{([\\s\\S]*?)\\}\\)\\);`)
75+
const fnMatch = content.match(fnDefRe)
76+
if (fnMatch) {
77+
const body = fnMatch[1]
78+
// Extract exports.X from this function body
79+
const innerDot = /exports\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g
80+
let im
81+
while ((im = innerDot.exec(body)) !== null) {
82+
if (im[1] !== '__esModule' && im[1] !== 'default') names.add(im[1])
83+
}
84+
}
85+
}
86+
87+
return Array.from(names)
88+
}
89+
90+
/**
91+
* Patch 2: Add named ESM re-exports for CJS barrel modules.
92+
*
93+
* Rolldown bundles CJS modules with only `export default require_xxx()` which
94+
* means `import { Foo } from 'package'` gets undefined. We detect these and
95+
* add explicit named re-exports by inspecting the actual CJS module at runtime.
96+
*/
97+
function patchNamedExports(files) {
98+
let count = 0
99+
// Match files that have `export default require_xxx()` as their main export
100+
const defaultExportPattern = /export default (require_\w+)\(\);/
101+
102+
for (const file of files) {
103+
const filePath = path.join(depsDir, file)
104+
const content = fs.readFileSync(filePath, 'utf-8')
105+
const match = content.match(defaultExportPattern)
106+
if (!match) continue
107+
108+
const requireFn = match[0] // e.g. "export default require_dist();"
109+
110+
// Try to get export names by requiring the actual CJS package.
111+
// Also try with browser globals shimmed to handle packages that use `self`, `window`, etc.
112+
const pkgName = file.replace(/\.js$/, '').replace(/_/g, '/')
113+
let exportKeys = []
114+
try {
115+
const cjsModule = require(pkgName)
116+
exportKeys = Object.keys(cjsModule).filter(
117+
k => k !== 'default' && k !== '__esModule' && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
118+
)
119+
} catch {
120+
// Package can't be loaded in Node (uses browser globals etc).
121+
// Try again with basic browser global shims.
122+
try {
123+
global.self = global
124+
global.window = global
125+
global.document = global.document || { createElement: () => ({}), addEventListener: () => {} }
126+
global.navigator = global.navigator || { userAgent: '' }
127+
// Clear require cache to retry with shims
128+
delete require.cache[require.resolve(pkgName)]
129+
const cjsModule = require(pkgName)
130+
exportKeys = Object.keys(cjsModule).filter(
131+
k => k !== 'default' && k !== '__esModule' && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
132+
)
133+
} catch {
134+
// Still can't load — fall back to static analysis plus
135+
// extracting exports from packages referenced via __exportStar
136+
exportKeys = extractExportNames(content)
137+
138+
// Also include exports from referenced ESM chunk files.
139+
// CJS barrels use `import { ... } from "./chunk.js"` then
140+
// `__exportStar(require_xxx(), exports)` to re-export sub-packages.
141+
// Scan all imported chunk files for their ESM exports.
142+
const chunkImportRe = /from "(\.\/([\w-]+)\.js)"/g
143+
let cim
144+
while ((cim = chunkImportRe.exec(content)) !== null) {
145+
const chunkPath = path.join(depsDir, cim[2] + '.js')
146+
if (!fs.existsSync(chunkPath)) continue
147+
const chunkContent = fs.readFileSync(chunkPath, 'utf-8')
148+
// Extract ESM export names from `export { A, B, C }`
149+
const esmExportRe = /export \{([^}]+)\}/g
150+
let em
151+
while ((em = esmExportRe.exec(chunkContent)) !== null) {
152+
for (const binding of em[1].split(',')) {
153+
const name = binding.trim().split(/\s+as\s+/).pop().trim()
154+
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) &&
155+
name !== 'default' && name !== '__esModule') {
156+
exportKeys.push(name)
157+
}
158+
}
159+
}
160+
// Also extract from `export var ...` patterns
161+
extractExportNames(chunkContent).forEach(k => exportKeys.push(k))
162+
}
163+
// Deduplicate
164+
exportKeys = [...new Set(exportKeys)]
165+
}
166+
}
167+
168+
// Filter out names that conflict with existing ESM-level declarations.
169+
// Only check import bindings and top-level var statements that are NOT
170+
// inside __commonJSMin wrappers (i.e., the few lines at the module boundary).
171+
const existingDecls = new Set()
172+
// Collect import bindings
173+
const importRe = /import\s*\{([^}]+)\}/g
174+
let dm
175+
while ((dm = importRe.exec(content)) !== null) {
176+
for (const binding of dm[1].split(',')) {
177+
const asMatch = binding.trim().match(/(?:\w+\s+as\s+)?(\w+)/)
178+
if (asMatch) existingDecls.add(asMatch[1])
179+
}
180+
}
181+
// Collect top-level var/function declarations outside CJS wrappers.
182+
// These are lines starting with "var " or "function " at column 0.
183+
const topDeclRe = /^(?:var|function)\s+([a-zA-Z_$]\w*)/gm
184+
while ((dm = topDeclRe.exec(content)) !== null) existingDecls.add(dm[1])
185+
// Also collect names already exported via `export { ... }` statements
186+
const existingExportRe = /export \{([^}]+)\}/g
187+
while ((dm = existingExportRe.exec(content)) !== null) {
188+
for (const binding of dm[1].split(',')) {
189+
// "require_Foo as bar" -> bar is the exported name
190+
const parts = binding.trim().split(/\s+as\s+/)
191+
const exportedName = parts[parts.length - 1].trim()
192+
if (exportedName) existingDecls.add(exportedName)
193+
}
194+
}
195+
196+
// Filter out JS reserved words and existing declarations
197+
const reserved = new Set([
198+
'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete',
199+
'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
200+
'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
201+
'void', 'while', 'with', 'class', 'const', 'enum', 'export', 'extends',
202+
'import', 'super', 'implements', 'interface', 'let', 'package', 'private',
203+
'protected', 'public', 'static', 'yield', 'await', 'async', 'of'
204+
])
205+
exportKeys = exportKeys.filter(k => !existingDecls.has(k) && !reserved.has(k))
206+
207+
if (exportKeys.length === 0) continue
208+
209+
// Replace `export default require_xxx();` with destructured named exports.
210+
// If CJS sets __esModule + exports.default, use .default as ESM default (esbuild compat).
211+
const fnName = match[1]
212+
const hasEsModuleDefault = (content.includes('exports.__esModule') || content.includes('"__esModule"')) &&
213+
!exportKeys.includes('default') && content.includes('exports.default')
214+
const defaultExpr = hasEsModuleDefault
215+
? `__cjs_mod__.__esModule ? __cjs_mod__.default : __cjs_mod__`
216+
: `__cjs_mod__`
217+
const replacement = [
218+
`var __cjs_mod__ = ${fnName}();`,
219+
`export default ${defaultExpr};`,
220+
`export var ${exportKeys.map(k => `${k} = __cjs_mod__["${k}"]`).join(',\n ')};`
221+
].join('\n')
222+
223+
const patched = content.replace(requireFn, replacement)
224+
if (patched !== content) {
225+
fs.writeFileSync(filePath, patched)
226+
count++
227+
console.log(` Added ${exportKeys.length} named exports to ${file}`)
228+
}
229+
}
230+
return count
231+
}
232+
233+
try {
234+
const files = fs.readdirSync(depsDir).filter(f => f.endsWith('.js'))
235+
236+
const toesmCount = patchToESM(files)
237+
const namedCount = patchNamedExports(files)
238+
239+
if (toesmCount + namedCount > 0) {
240+
console.log(`Patched ${toesmCount} chunk(s) for __toESM, ${namedCount} chunk(s) for named exports.`)
241+
242+
// Update _metadata.json file hashes so Vite doesn't detect stale deps
243+
// and re-optimize on next start (which would overwrite our patches).
244+
const metadataPath = path.join(depsDir, '_metadata.json')
245+
try {
246+
const crypto = require('crypto')
247+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
248+
const optimized = metadata.optimized || {}
249+
for (const [, entry] of Object.entries(optimized)) {
250+
if (entry.file) {
251+
const filePath = path.join(depsDir, '..', '..', entry.file)
252+
if (fs.existsSync(filePath)) {
253+
const content = fs.readFileSync(filePath)
254+
entry.fileHash = crypto.createHash('sha256').update(content).digest('hex').substring(0, 8)
255+
}
256+
}
257+
}
258+
// Also update the chunks section if present
259+
const chunks = metadata.chunks || {}
260+
for (const [, entry] of Object.entries(chunks)) {
261+
if (entry.file) {
262+
const filePath = path.join(depsDir, '..', '..', entry.file)
263+
if (fs.existsSync(filePath)) {
264+
const content = fs.readFileSync(filePath)
265+
entry.fileHash = crypto.createHash('sha256').update(content).digest('hex').substring(0, 8)
266+
}
267+
}
268+
}
269+
// Also update the browserHash so the v= query param changes,
270+
// busting the browser's HTTP cache for the patched dep files.
271+
const newBrowserHash = crypto.randomBytes(4).toString('hex')
272+
metadata.browserHash = newBrowserHash
273+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2))
274+
console.log(`Updated _metadata.json (browserHash: ${newBrowserHash}).`)
275+
} catch (e) {
276+
console.warn('Warning: could not update _metadata.json:', e.message)
277+
}
278+
} else {
279+
console.log('No dep chunks needed patching.')
280+
}
281+
} catch (e) {
282+
if (e.code === 'ENOENT') {
283+
console.log('No .vite/deps directory found — deps will be optimized on first request.')
284+
} else {
285+
throw e
286+
}
287+
}

webapp/src/components/AssetFilters/AssetFilters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useMemo } from 'react'
22
import { EmoteOutcomeType, EmotePlayMode, GenderFilterOption, Network, Rarity, WearableGender } from '@dcl/schemas'
33
import { RarityFilter } from 'decentraland-dapps/dist/containers/RarityFilter'
4-
import { BarChartSource } from 'decentraland-ui/lib/components/BarChart/BarChart.types'
4+
import { BarChartSource } from 'decentraland-ui/dist/components/BarChart/BarChart.types'
55
import { getSectionFromCategory } from '../../modules/routing/search'
66
import { Sections, SortBy, BrowseOptions } from '../../modules/routing/types'
77
import { View } from '../../modules/ui/types'

webapp/src/components/AssetFilters/EstateSizeFilter/EstateSizeFilter.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Network } from '@dcl/schemas/dist/dapps/network'
2-
import { BarChartSource } from 'decentraland-ui/lib/components/BarChart/BarChart.types'
2+
import { BarChartSource } from 'decentraland-ui/dist/components/BarChart/BarChart.types'
33
import { BrowseOptions } from '../../../modules/routing/types'
44
import { LANDFilters } from '../../Vendor/decentraland/types'
55

webapp/src/components/AssetFilters/Inventory/Inventory.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BarChartProps } from 'decentraland-ui/lib/components/BarChart/BarChart.types'
1+
import { BarChartProps } from 'decentraland-ui/dist/components/BarChart/BarChart.types'
22
import { BrowseOptions } from '../../../modules/routing/types'
33

44
export type Props = {

0 commit comments

Comments
 (0)