[{"data":1,"prerenderedAt":1676},["ShallowReactive",2],{"post-\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin":3},{"id":4,"title":5,"body":6,"contentId":1659,"date":1660,"description":1661,"extension":1662,"meta":1663,"navigation":422,"path":1664,"rawbody":1665,"robots":1666,"schemaOrg":1666,"seo":1667,"sitemap":1668,"stem":1674,"__hash__":1675},"posts\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin.md","Laravel and Vite: A Love Story Ruined with Cross-Origin",{"type":7,"value":8,"toc":1652},"minimark",[9,16,24,42,47,56,62,71,79,82,89,92,102,161,170,176,179,183,195,201,204,207,210,225,228,383,386,531,542,545,560,564,567,570,578,588,596,599,643,646,690,693,709,712,783,786,789,792,986,989,1002,1013,1017,1020,1023,1034,1041,1144,1156,1159,1168,1174,1177,1186,1189,1192,1199,1465,1478,1481,1484,1487,1490,1505,1526,1531,1607,1614,1617,1623,1626,1629,1635,1639,1642,1648],[10,11,12],"p",{},[13,14,15],"em",{},"This title might sound dramatic, but trust me, it's worth your read. Dive in!",[10,17,18,19,23],{},"It's great to be back to my blog. I've been away due to numerous conference preparations and a major internal updates to this website. Although these aren't today's topics, I'll certainly cover them in future posts. Also, this is my first article on ",[20,21,22],"strong",{},"Laravel",", and it won't be the last!",[10,25,26,27,29,30,33,34,41],{},"Today, I want to share an experience I had with ",[20,28,22],{}," and ",[20,31,32],{},"Vite"," because of the strictness of the ",[35,36,40],"a",{"href":37,"rel":38},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FUsing_web_workers",[39],"nofollow","Web Worker API",".",[43,44,46],"h2",{"id":45},"the-laravel-and-vite-integration","The Laravel and Vite integration",[10,48,49,50,52,53,55],{},"Before delving into the problem, it's crucial to understand how ",[20,51,32],{}," operates within a ",[20,54,22],{}," project.",[10,57,58,59,61],{},"First, what is ",[20,60,32],{},"? It's a fast frontend build tool that powers next-gen web applications. It allows you to construct your frontend assets (JavaScript, CSS, etc.) efficiently, especially during development. The core of Vite is a plugin pipeline that transforms source files into the required outputs, a point we'll revisit soon.",[10,63,64,65,67,68],{},"And ",[20,66,22],{},"? It's a web application framework known for its expressive, elegant syntax. Laravel eases common tasks like authentication, routing, sessions, and caching, aiming to make development enjoyable without compromising functionality. ",[13,69,70],{},"And it excels at it!",[10,72,73,74,29,76,78],{},"When you embrace ",[20,75,22],{},[20,77,32],{},", you get a formidable stack for web application development.",[10,80,81],{},"Sounds great, right? But how does it works practically? Let me illustrate.",[10,83,84],{},[85,86],"img",{"alt":87,"src":88},"Laravel and Vite integration","\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Flaravel-vite-request-response.png",[10,90,91],{},"This image offers a high-level overview of the Laravel and Vite architecture. During development, two servers run: one for Laravel and one for Vite.",[93,94,95,99],"ol",{},[96,97,98],"li",{},"A user requests a page from the Laravel server.",[96,100,101],{},"The Laravel server sends the HTML page to the user. Before sending the response, Laravel adds a link tag to the entry script served by the Vite server. For example, you might find this line in the HTML returned by the Laravel server:",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript type=\"module\" src=\"http:\u002F\u002Flocalhost:5173\u002Findex.ts\">\u003C\u002Fscript>\n","html","",[110,111,112],"code",{"__ignoreMap":108},[113,114,117,121,125,129,132,135,139,141,144,146,148,151,153,156,158],"span",{"class":115,"line":116},"line",1,[113,118,120],{"class":119},"sMK4o","\u003C",[113,122,124],{"class":123},"swJcz","script",[113,126,128],{"class":127},"spNyl"," type",[113,130,131],{"class":119},"=",[113,133,134],{"class":119},"\"",[113,136,138],{"class":137},"sfazB","module",[113,140,134],{"class":119},[113,142,143],{"class":127}," src",[113,145,131],{"class":119},[113,147,134],{"class":119},[113,149,150],{"class":137},"http:\u002F\u002Flocalhost:5173\u002Findex.ts",[113,152,134],{"class":119},[113,154,155],{"class":119},">\u003C\u002F",[113,157,124],{"class":123},[113,159,160],{"class":119},">\n",[93,162,164,167],{"start":163},3,[96,165,166],{},"The user's browser loads the HTML and detects the script tag pointing to the Vite server, then requests the script from the Vite server.",[96,168,169],{},"The Vite server delivers the script to the user's browser.",[10,171,172,173],{},"This process is straightforward and highly effective during development. It gives you access to Vite's full power with the plugin pipeline and hot module replacement (HMR). ",[13,174,175],{},"Many developers might not fully appreciate how seamlessly this integration works compared to other tools.",[10,177,178],{},"However, sometimes things get tricky. And that's where our story begins.",[43,180,182],{"id":181},"the-context","The context",[10,184,185,186,191,192],{},"For a project, I had to use the ",[35,187,190],{"href":188,"rel":189},"https:\u002F\u002Fmicrosoft.github.io\u002Fmonaco-editor\u002F",[39],"Monaco Editor",", a browser-based code editor and the core of Visual Studio Code. The aim was to display a JSON in the code editor and validate it against a JSON schema. Users can edit this JSON, and the Monaco Editor, with JSON schema validation, will show in real time whether the JSON is valid or suggest corrections. Amazingly efficient when it works! ",[13,193,194],{},"And it has been much more complexe than expected.",[10,196,197],{},[85,198],{"alt":199,"src":200},"Monaco Editor with a familiar interface if you're using Visual Studio Code","\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Fmonaco-editor.png",[10,202,203],{},"To achieve this, the Monaco Editor requires starting some Web Workers. Why and what are Web Workers?",[10,205,206],{},"JavaScript, especially in browser environments, operates on a single thread, meaning it executes one task at a time. So, if a piece of code takes long to execute, the browser becomes unresponsive which degrades the user experience. This is why web developers advise against blocking the main thread.",[10,208,209],{},"To handle long tasks without affecting performance, we have two options:",[211,212,213,219],"ul",{},[96,214,215,218],{},[20,216,217],{},"Asynchronous JavaScript",": This involves breaking down tasks into smaller chunks and executing them asynchronously. This way, the main thread remains free to handle other tasks.",[96,220,221,224],{},[20,222,223],{},"Web Workers",": For more complex tasks, especially those requiring heavy computation, we use Web Workers. A Web Worker is essentially a JavaScript file running in the background, separate from other scripts, with which it can communicate via messages.",[10,226,227],{},"Here's a simple example of a Web Worker script:",[103,229,234],{"className":230,"code":231,"filename":232,"language":233,"meta":108,"style":108},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","globThis.onmessage = function (e) {\n  console.log('Message received from main script')\n  const workerResult = `Result: ${e.data[0] * e.data[1]}`\n  console.log('Posting message back to main script')\n  postMessage(workerResult)\n}\n","worker.js","ts",[110,235,236,267,292,344,364,377],{"__ignoreMap":108},[113,237,238,242,244,248,251,254,257,261,264],{"class":115,"line":116},[113,239,241],{"class":240},"sTEyZ","globThis",[113,243,41],{"class":119},[113,245,247],{"class":246},"s2Zo4","onmessage",[113,249,250],{"class":119}," =",[113,252,253],{"class":127}," function",[113,255,256],{"class":119}," (",[113,258,260],{"class":259},"sHdIc","e",[113,262,263],{"class":119},")",[113,265,266],{"class":119}," {\n",[113,268,270,273,275,278,281,284,287,289],{"class":115,"line":269},2,[113,271,272],{"class":240},"  console",[113,274,41],{"class":119},[113,276,277],{"class":246},"log",[113,279,280],{"class":123},"(",[113,282,283],{"class":119},"'",[113,285,286],{"class":137},"Message received from main script",[113,288,283],{"class":119},[113,290,291],{"class":123},")\n",[113,293,294,297,300,302,305,308,311,313,315,318,322,325,328,331,333,335,338,341],{"class":115,"line":163},[113,295,296],{"class":127},"  const",[113,298,299],{"class":240}," workerResult",[113,301,250],{"class":119},[113,303,304],{"class":119}," `",[113,306,307],{"class":137},"Result: ",[113,309,310],{"class":119},"${",[113,312,260],{"class":240},[113,314,41],{"class":119},[113,316,317],{"class":240},"data[",[113,319,321],{"class":320},"sbssI","0",[113,323,324],{"class":240},"] ",[113,326,327],{"class":119},"*",[113,329,330],{"class":240}," e",[113,332,41],{"class":119},[113,334,317],{"class":240},[113,336,337],{"class":320},"1",[113,339,340],{"class":240},"]",[113,342,343],{"class":119},"}`\n",[113,345,347,349,351,353,355,357,360,362],{"class":115,"line":346},4,[113,348,272],{"class":240},[113,350,41],{"class":119},[113,352,277],{"class":246},[113,354,280],{"class":123},[113,356,283],{"class":119},[113,358,359],{"class":137},"Posting message back to main script",[113,361,283],{"class":119},[113,363,291],{"class":123},[113,365,367,370,372,375],{"class":115,"line":366},5,[113,368,369],{"class":246},"  postMessage",[113,371,280],{"class":123},[113,373,374],{"class":240},"workerResult",[113,376,291],{"class":123},[113,378,380],{"class":115,"line":379},6,[113,381,382],{"class":119},"}\n",[10,384,385],{},"With its main script:",[103,387,390],{"className":230,"code":388,"filename":389,"language":233,"meta":108,"style":108},"const worker = new Worker('worker.js')\n\nworker.addEventListener('message', (e) => {\n  console.log('Message received from worker')\n  console.log(e.data)\n})\n\nworker.postMessage([2, 3])\n","main.js",[110,391,392,418,424,457,476,495,502,507],{"__ignoreMap":108},[113,393,394,397,400,402,405,408,410,412,414,416],{"class":115,"line":116},[113,395,396],{"class":127},"const",[113,398,399],{"class":240}," worker ",[113,401,131],{"class":119},[113,403,404],{"class":119}," new",[113,406,407],{"class":246}," Worker",[113,409,280],{"class":240},[113,411,283],{"class":119},[113,413,232],{"class":137},[113,415,283],{"class":119},[113,417,291],{"class":240},[113,419,420],{"class":115,"line":269},[113,421,423],{"emptyLinePlaceholder":422},true,"\n",[113,425,426,429,431,434,436,438,441,443,446,448,450,452,455],{"class":115,"line":163},[113,427,428],{"class":240},"worker",[113,430,41],{"class":119},[113,432,433],{"class":246},"addEventListener",[113,435,280],{"class":240},[113,437,283],{"class":119},[113,439,440],{"class":137},"message",[113,442,283],{"class":119},[113,444,445],{"class":119},",",[113,447,256],{"class":119},[113,449,260],{"class":259},[113,451,263],{"class":119},[113,453,454],{"class":127}," =>",[113,456,266],{"class":119},[113,458,459,461,463,465,467,469,472,474],{"class":115,"line":346},[113,460,272],{"class":240},[113,462,41],{"class":119},[113,464,277],{"class":246},[113,466,280],{"class":123},[113,468,283],{"class":119},[113,470,471],{"class":137},"Message received from worker",[113,473,283],{"class":119},[113,475,291],{"class":123},[113,477,478,480,482,484,486,488,490,493],{"class":115,"line":366},[113,479,272],{"class":240},[113,481,41],{"class":119},[113,483,277],{"class":246},[113,485,280],{"class":123},[113,487,260],{"class":240},[113,489,41],{"class":119},[113,491,492],{"class":240},"data",[113,494,291],{"class":123},[113,496,497,500],{"class":115,"line":379},[113,498,499],{"class":119},"}",[113,501,291],{"class":240},[113,503,505],{"class":115,"line":504},7,[113,506,423],{"emptyLinePlaceholder":422},[113,508,510,512,514,517,520,523,525,528],{"class":115,"line":509},8,[113,511,428],{"class":240},[113,513,41],{"class":119},[113,515,516],{"class":246},"postMessage",[113,518,519],{"class":240},"([",[113,521,522],{"class":320},"2",[113,524,445],{"class":119},[113,526,527],{"class":320}," 3",[113,529,530],{"class":240},"])\n",[10,532,533,534,536,537,539,540,41],{},"In this example, the main script creates a new worker and sends it a message using ",[110,535,516],{},". The worker receives and responds with ",[110,538,516],{},". Due to JavaScript's event-driven nature, the main script listens for the worker's message using ",[110,541,433],{},[10,543,544],{},"In our case, Monaco Editor leverages Web Workers for certain features, including JSON schema validation.",[546,547,548],"blockquote",{},[10,549,550,553,554,559],{},[113,551,552],{},"!NOTE","\nUsing a Web Worker allows the Monaco Editor to run a ",[35,555,558],{"href":556,"rel":557},"https:\u002F\u002Fmicrosoft.github.io\u002Flanguage-server-protocol\u002F",[39],"LSP"," server in the background for each language, providing more advanced features without impacting the main thread and the user experience.",[43,561,563],{"id":562},"the-issue","The issue",[10,565,566],{},"But here's the catch:",[10,568,569],{},"The script passed to the Web Worker must be accessible from the same origin as the main script. This is where the problem arises.",[546,571,572],{},[10,573,574,577],{},[113,575,576],{},"!WARNING","\nThis is a security feature to prevent cross-origin attacks so it's impossible to bypass it.",[10,579,580,581,29,584,587],{},"An origin is defined by a URL's scheme, host, and port. This means ",[110,582,583],{},"https:\u002F\u002Fexample.com",[110,585,586],{},"https:\u002F\u002Fexample.com:1024"," are considered different origins.",[10,589,590,591,593,594,41],{},"Imagine we're on the page ",[110,592,583],{}," and you want to create a Web Worker. The script URL passed to the Web Worker must be accessible from ",[110,595,583],{},[10,597,598],{},"This means that this works:",[103,600,602],{"className":105,"code":601,"language":107,"meta":108,"style":108},"\u003Cscript>\n  const worker = new Worker('worker.js')\n\u003C\u002Fscript>\n",[110,603,604,612,634],{"__ignoreMap":108},[113,605,606,608,610],{"class":115,"line":116},[113,607,120],{"class":119},[113,609,124],{"class":123},[113,611,160],{"class":119},[113,613,614,616,618,620,622,624,626,628,630,632],{"class":115,"line":269},[113,615,296],{"class":127},[113,617,399],{"class":240},[113,619,131],{"class":119},[113,621,404],{"class":119},[113,623,407],{"class":246},[113,625,280],{"class":240},[113,627,283],{"class":119},[113,629,232],{"class":137},[113,631,283],{"class":119},[113,633,291],{"class":240},[113,635,636,639,641],{"class":115,"line":163},[113,637,638],{"class":119},"\u003C\u002F",[113,640,124],{"class":123},[113,642,160],{"class":119},[10,644,645],{},"But this doesn't:",[103,647,649],{"className":105,"code":648,"language":107,"meta":108,"style":108},"\u003Cscript>\n  const worker = new Worker('https:\u002F\u002Fanother-origin.example.com\u002Fworker.js')\n\u003C\u002Fscript>\n",[110,650,651,659,682],{"__ignoreMap":108},[113,652,653,655,657],{"class":115,"line":116},[113,654,120],{"class":119},[113,656,124],{"class":123},[113,658,160],{"class":119},[113,660,661,663,665,667,669,671,673,675,678,680],{"class":115,"line":269},[113,662,296],{"class":127},[113,664,399],{"class":240},[113,666,131],{"class":119},[113,668,404],{"class":119},[113,670,407],{"class":246},[113,672,280],{"class":240},[113,674,283],{"class":119},[113,676,677],{"class":137},"https:\u002F\u002Fanother-origin.example.com\u002Fworker.js",[113,679,283],{"class":119},[113,681,291],{"class":240},[113,683,684,686,688],{"class":115,"line":163},[113,685,638],{"class":119},[113,687,124],{"class":123},[113,689,160],{"class":119},[10,691,692],{},"Do you see the issue now?",[10,694,695,696,699,700,703,704,29,706,708],{},"Our Laravel server runs on ",[110,697,698],{},"http:\u002F\u002Flocalhost:8000",", while Vite is on ",[110,701,702],{},"http:\u002F\u002Flocalhost:5173",". ",[110,705,698],{},[110,707,702],{}," are two different origins. This means the browser won't load the Web Worker script from the Vite server, resulting in a cross-origin issue due to running dual servers.",[10,710,711],{},"In the browser, we can easily spot this error with the following warning:",[103,713,717],{"className":714,"code":715,"language":716,"meta":108,"style":108},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","app.js:21 Failed to construct 'Worker': Script at 'http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker.js?worker_file&type=module' cannot be accessed from origin 'http:\u002F\u002Flocalhost:'.\n","sh",[110,718,719],{"__ignoreMap":108},[113,720,721,725,728,731,734,737,740,742,745,748,751,753,756,758,761,764,767,770,773,775,778,780],{"class":115,"line":116},[113,722,724],{"class":723},"sBMFI","app.js:21",[113,726,727],{"class":137}," Failed",[113,729,730],{"class":137}," to",[113,732,733],{"class":137}," construct",[113,735,736],{"class":119}," '",[113,738,739],{"class":137},"Worker",[113,741,283],{"class":119},[113,743,744],{"class":137},":",[113,746,747],{"class":137}," Script",[113,749,750],{"class":137}," at",[113,752,736],{"class":119},[113,754,755],{"class":137},"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker.js?worker_file&type=module",[113,757,283],{"class":119},[113,759,760],{"class":137}," cannot",[113,762,763],{"class":137}," be",[113,765,766],{"class":137}," accessed",[113,768,769],{"class":137}," from",[113,771,772],{"class":137}," origin",[113,774,736],{"class":119},[113,776,777],{"class":137},"http:\u002F\u002Flocalhost:",[113,779,283],{"class":119},[113,781,782],{"class":137},".\n",[10,784,785],{},"This error is a showstopper. The Monaco Editor can't function properly without the Web Worker script. Let's find a solution.",[10,787,788],{},"Spoiler alert, I searched high and low online for a working solution. Sadly, I couldn't find one. So I try to tackle the issue myself. After extensive research and trials, I found a solution that I'm sharing it with you in this article.",[10,790,791],{},"The problematic code looks like this:",[103,793,795],{"className":230,"code":794,"language":233,"meta":108,"style":108},"import * as monaco from 'monaco-editor'\nimport EditorWorker from 'monaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker?worker'\nimport JsonWorker from 'monaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker?worker'\n\nglobalThis.MonacoEnvironment = {\n  getWorker(workerId, label) {\n    switch (label) {\n      case 'json':\n        return new JsonWorker()\n      default:\n        return new EditorWorker()\n    }\n  },\n}\n\n\u002F\u002F Monaco Editor initialization and Diagnostic feature\n",[110,796,797,823,839,855,859,873,892,908,923,937,945,957,963,969,974,979],{"__ignoreMap":108},[113,798,799,803,806,809,812,815,817,820],{"class":115,"line":116},[113,800,802],{"class":801},"s7zQu","import",[113,804,805],{"class":119}," *",[113,807,808],{"class":801}," as",[113,810,811],{"class":240}," monaco ",[113,813,814],{"class":801},"from",[113,816,736],{"class":119},[113,818,819],{"class":137},"monaco-editor",[113,821,822],{"class":119},"'\n",[113,824,825,827,830,832,834,837],{"class":115,"line":269},[113,826,802],{"class":801},[113,828,829],{"class":240}," EditorWorker ",[113,831,814],{"class":801},[113,833,736],{"class":119},[113,835,836],{"class":137},"monaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker?worker",[113,838,822],{"class":119},[113,840,841,843,846,848,850,853],{"class":115,"line":163},[113,842,802],{"class":801},[113,844,845],{"class":240}," JsonWorker ",[113,847,814],{"class":801},[113,849,736],{"class":119},[113,851,852],{"class":137},"monaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker?worker",[113,854,822],{"class":119},[113,856,857],{"class":115,"line":346},[113,858,423],{"emptyLinePlaceholder":422},[113,860,861,864,866,869,871],{"class":115,"line":366},[113,862,863],{"class":240},"globalThis",[113,865,41],{"class":119},[113,867,868],{"class":240},"MonacoEnvironment ",[113,870,131],{"class":119},[113,872,266],{"class":119},[113,874,875,878,880,883,885,888,890],{"class":115,"line":379},[113,876,877],{"class":123},"  getWorker",[113,879,280],{"class":119},[113,881,882],{"class":259},"workerId",[113,884,445],{"class":119},[113,886,887],{"class":259}," label",[113,889,263],{"class":119},[113,891,266],{"class":119},[113,893,894,897,899,902,905],{"class":115,"line":504},[113,895,896],{"class":801},"    switch",[113,898,256],{"class":123},[113,900,901],{"class":240},"label",[113,903,904],{"class":123},") ",[113,906,907],{"class":119},"{\n",[113,909,910,913,915,918,920],{"class":115,"line":509},[113,911,912],{"class":801},"      case",[113,914,736],{"class":119},[113,916,917],{"class":137},"json",[113,919,283],{"class":119},[113,921,922],{"class":119},":\n",[113,924,926,929,931,934],{"class":115,"line":925},9,[113,927,928],{"class":801},"        return",[113,930,404],{"class":119},[113,932,933],{"class":246}," JsonWorker",[113,935,936],{"class":123},"()\n",[113,938,940,943],{"class":115,"line":939},10,[113,941,942],{"class":801},"      default",[113,944,922],{"class":119},[113,946,948,950,952,955],{"class":115,"line":947},11,[113,949,928],{"class":801},[113,951,404],{"class":119},[113,953,954],{"class":246}," EditorWorker",[113,956,936],{"class":123},[113,958,960],{"class":115,"line":959},12,[113,961,962],{"class":119},"    }\n",[113,964,966],{"class":115,"line":965},13,[113,967,968],{"class":119},"  },\n",[113,970,972],{"class":115,"line":971},14,[113,973,382],{"class":119},[113,975,977],{"class":115,"line":976},15,[113,978,423],{"emptyLinePlaceholder":422},[113,980,982],{"class":115,"line":981},16,[113,983,985],{"class":984},"sHwdD","\u002F\u002F Monaco Editor initialization and Diagnostic feature\n",[10,987,988],{},"When Monaco is initialized, it tries to load the Web Worker script from the Vite server. This is where the cross-origin issue arises.",[546,990,991],{},[10,992,993,995,996,1001],{},[113,994,552],{},"\nIn frameworks like ",[35,997,1000],{"href":998,"rel":999},"https:\u002F\u002Fadonisjs.com\u002F",[39],"Adonis",", where Vite serves as middleware, this issue doesn't occur because Vite runs within the same application as the server. But in Laravel, with two separate servers, we needed a workaround.",[546,1003,1004],{},[10,1005,1006,1008,1009,1012],{},[113,1007,552],{},"\nThis issue doesn't exist if assets are built and loaded from ",[110,1010,1011],{},"\u002Fpublic\u002Fbuild",", as everything is served by the Laravel server. However, this significantly impacts the developer experience, requiring asset rebuilding and page refreshing for every change. This isn't aligned with Vite's experience or its on-demand philosophy.",[43,1014,1016],{"id":1015},"the-solution","The solution",[10,1018,1019],{},"Now we have a clear understanding of the issue and the origin of the problem. Let's dive into the solution.",[10,1021,1022],{},"First, we can examine the worker script URL:",[103,1024,1028],{"className":1025,"code":1026,"language":1027,"meta":108,"style":108},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","monaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker?worker\n","txt",[110,1029,1030],{"__ignoreMap":108},[113,1031,1032],{"class":115,"line":116},[113,1033,1026],{},[10,1035,1036,1037,1040],{},"It's a worker script, not intended for direct browser loading. Notice the ",[110,1038,1039],{},"?worker"," query parameter, a Vite hint to wrap the script for easy use. We can inspect the response to have a better understanding.",[103,1042,1046],{"className":1043,"code":1044,"language":1045,"meta":108,"style":108},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export default function WorkerWrapper(options) {\n  return new Worker(\n    'http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module',\n    {\n      type: 'module',\n      name: options?.name\n    }\n  )\n}\n","js",[110,1047,1048,1070,1082,1095,1100,1115,1131,1135,1140],{"__ignoreMap":108},[113,1049,1050,1053,1056,1058,1061,1063,1066,1068],{"class":115,"line":116},[113,1051,1052],{"class":801},"export",[113,1054,1055],{"class":801}," default",[113,1057,253],{"class":127},[113,1059,1060],{"class":246}," WorkerWrapper",[113,1062,280],{"class":119},[113,1064,1065],{"class":259},"options",[113,1067,263],{"class":119},[113,1069,266],{"class":119},[113,1071,1072,1075,1077,1079],{"class":115,"line":269},[113,1073,1074],{"class":801},"  return",[113,1076,404],{"class":119},[113,1078,407],{"class":246},[113,1080,1081],{"class":123},"(\n",[113,1083,1084,1087,1090,1092],{"class":115,"line":163},[113,1085,1086],{"class":119},"    '",[113,1088,1089],{"class":137},"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module",[113,1091,283],{"class":119},[113,1093,1094],{"class":119},",\n",[113,1096,1097],{"class":115,"line":346},[113,1098,1099],{"class":119},"    {\n",[113,1101,1102,1105,1107,1109,1111,1113],{"class":115,"line":366},[113,1103,1104],{"class":123},"      type",[113,1106,744],{"class":119},[113,1108,736],{"class":119},[113,1110,138],{"class":137},[113,1112,283],{"class":119},[113,1114,1094],{"class":119},[113,1116,1117,1120,1122,1125,1128],{"class":115,"line":379},[113,1118,1119],{"class":123},"      name",[113,1121,744],{"class":119},[113,1123,1124],{"class":240}," options",[113,1126,1127],{"class":119},"?.",[113,1129,1130],{"class":240},"name\n",[113,1132,1133],{"class":115,"line":504},[113,1134,962],{"class":119},[113,1136,1137],{"class":115,"line":509},[113,1138,1139],{"class":123},"  )\n",[113,1141,1142],{"class":115,"line":925},[113,1143,382],{"class":119},[10,1145,1146,1147,1149,1150,1152,1153,1155],{},"Thanks to the ",[110,1148,1039],{}," query parameter, Vite automatically wraps the script in a function that returns a new ",[110,1151,739],{}," instance. So when we import the script, we can easily invoke the function to start the worker without having to manually create a new ",[110,1154,739],{}," instance with the correct URL and options.",[10,1157,1158],{},"The worker use the following script:",[103,1160,1162],{"className":1025,"code":1161,"language":1027,"meta":108,"style":108},"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module\n",[110,1163,1164],{"__ignoreMap":108},[113,1165,1166],{"class":115,"line":116},[113,1167,1161],{},[10,1169,1170,1171,1173],{},"The ",[110,1172,702],{}," origin belongs to the Vite server and it's the root of the issue.",[10,1175,1176],{},"To be successful, the script URL must match our Laravel server's origin:",[103,1178,1180],{"className":1025,"code":1179,"language":1027,"meta":108,"style":108},"http:\u002F\u002Flocalhost:8000\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module\n",[110,1181,1182],{"__ignoreMap":108},[113,1183,1184],{"class":115,"line":116},[113,1185,1179],{},[10,1187,1188],{},"But how to modify the URL dynamically to match the Laravel server origin?",[10,1190,1191],{},"With a Vite plugin! It's the core of Vite to transform source files on the fly. With a plugin, we could intercept the worker script request and adjust the URL to match the Laravel server origin.",[10,1193,1194,1195,1198],{},"In our ",[110,1196,1197],{},"vite.config.ts",", we can define a plugin to rewrite the worker script URL:",[103,1200,1202],{"className":230,"code":1201,"language":233,"meta":108,"style":108},"import { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [\n    (() => {\n      return {\n        name: 'monaco-editor:rewrite-worker',\n        transform(code, id) {\n          if (this.environment.mode !== 'dev') {\n            return\n          }\n\n          if (id.includes('worker')) {\n            return code.replace(\n              '__laravel_vite_placeholder__.test',\n              'localhost:8000',\n            )\n          }\n        },\n      }\n    })(),\n  ],\n})\n",[110,1203,1204,1226,1230,1242,1252,1264,1271,1287,1305,1337,1342,1347,1351,1378,1393,1405,1416,1422,1427,1433,1439,1450,1458],{"__ignoreMap":108},[113,1205,1206,1208,1211,1214,1217,1219,1221,1224],{"class":115,"line":116},[113,1207,802],{"class":801},[113,1209,1210],{"class":119}," {",[113,1212,1213],{"class":240}," defineConfig",[113,1215,1216],{"class":119}," }",[113,1218,769],{"class":801},[113,1220,736],{"class":119},[113,1222,1223],{"class":137},"vite",[113,1225,822],{"class":119},[113,1227,1228],{"class":115,"line":269},[113,1229,423],{"emptyLinePlaceholder":422},[113,1231,1232,1234,1236,1238,1240],{"class":115,"line":163},[113,1233,1052],{"class":801},[113,1235,1055],{"class":801},[113,1237,1213],{"class":246},[113,1239,280],{"class":240},[113,1241,907],{"class":119},[113,1243,1244,1247,1249],{"class":115,"line":346},[113,1245,1246],{"class":123},"  plugins",[113,1248,744],{"class":119},[113,1250,1251],{"class":240}," [\n",[113,1253,1254,1257,1260,1262],{"class":115,"line":366},[113,1255,1256],{"class":240},"    (",[113,1258,1259],{"class":119},"()",[113,1261,454],{"class":127},[113,1263,266],{"class":119},[113,1265,1266,1269],{"class":115,"line":379},[113,1267,1268],{"class":801},"      return",[113,1270,266],{"class":119},[113,1272,1273,1276,1278,1280,1283,1285],{"class":115,"line":504},[113,1274,1275],{"class":123},"        name",[113,1277,744],{"class":119},[113,1279,736],{"class":119},[113,1281,1282],{"class":137},"monaco-editor:rewrite-worker",[113,1284,283],{"class":119},[113,1286,1094],{"class":119},[113,1288,1289,1292,1294,1296,1298,1301,1303],{"class":115,"line":509},[113,1290,1291],{"class":123},"        transform",[113,1293,280],{"class":119},[113,1295,110],{"class":259},[113,1297,445],{"class":119},[113,1299,1300],{"class":259}," id",[113,1302,263],{"class":119},[113,1304,266],{"class":119},[113,1306,1307,1310,1312,1315,1318,1320,1323,1326,1328,1331,1333,1335],{"class":115,"line":925},[113,1308,1309],{"class":801},"          if",[113,1311,256],{"class":123},[113,1313,1314],{"class":119},"this.",[113,1316,1317],{"class":240},"environment",[113,1319,41],{"class":119},[113,1321,1322],{"class":240},"mode",[113,1324,1325],{"class":119}," !==",[113,1327,736],{"class":119},[113,1329,1330],{"class":137},"dev",[113,1332,283],{"class":119},[113,1334,904],{"class":123},[113,1336,907],{"class":119},[113,1338,1339],{"class":115,"line":939},[113,1340,1341],{"class":801},"            return\n",[113,1343,1344],{"class":115,"line":947},[113,1345,1346],{"class":119},"          }\n",[113,1348,1349],{"class":115,"line":959},[113,1350,423],{"emptyLinePlaceholder":422},[113,1352,1353,1355,1357,1360,1362,1365,1367,1369,1371,1373,1376],{"class":115,"line":965},[113,1354,1309],{"class":801},[113,1356,256],{"class":123},[113,1358,1359],{"class":240},"id",[113,1361,41],{"class":119},[113,1363,1364],{"class":246},"includes",[113,1366,280],{"class":123},[113,1368,283],{"class":119},[113,1370,428],{"class":137},[113,1372,283],{"class":119},[113,1374,1375],{"class":123},")) ",[113,1377,907],{"class":119},[113,1379,1380,1383,1386,1388,1391],{"class":115,"line":971},[113,1381,1382],{"class":801},"            return",[113,1384,1385],{"class":240}," code",[113,1387,41],{"class":119},[113,1389,1390],{"class":246},"replace",[113,1392,1081],{"class":123},[113,1394,1395,1398,1401,1403],{"class":115,"line":976},[113,1396,1397],{"class":119},"              '",[113,1399,1400],{"class":137},"__laravel_vite_placeholder__.test",[113,1402,283],{"class":119},[113,1404,1094],{"class":119},[113,1406,1407,1409,1412,1414],{"class":115,"line":981},[113,1408,1397],{"class":119},[113,1410,1411],{"class":137},"localhost:8000",[113,1413,283],{"class":119},[113,1415,1094],{"class":119},[113,1417,1419],{"class":115,"line":1418},17,[113,1420,1421],{"class":123},"            )\n",[113,1423,1425],{"class":115,"line":1424},18,[113,1426,1346],{"class":119},[113,1428,1430],{"class":115,"line":1429},19,[113,1431,1432],{"class":119},"        },\n",[113,1434,1436],{"class":115,"line":1435},20,[113,1437,1438],{"class":119},"      }\n",[113,1440,1442,1445,1448],{"class":115,"line":1441},21,[113,1443,1444],{"class":119},"    }",[113,1446,1447],{"class":240},")()",[113,1449,1094],{"class":119},[113,1451,1453,1456],{"class":115,"line":1452},22,[113,1454,1455],{"class":240},"  ]",[113,1457,1094],{"class":119},[113,1459,1461,1463],{"class":115,"line":1460},23,[113,1462,499],{"class":119},[113,1464,291],{"class":240},[546,1466,1467],{},[10,1468,1469,1471,1472,1474,1475,1477],{},[113,1470,552],{},"\nHere, we're replacing ",[110,1473,1400],{}," instead of ",[110,1476,702],{},", as this URL is typically managed by the Laravel Vite plugin.",[10,1479,1480],{},"With this plugin, we can now load the Monaco Editor worker script since the origin matches the Laravel server. In production, the script URL will be correct and working as expected so we don't need to rewrite the URL. Build files are served from the same origin using a single server like Nginx, Apache or Caddy.",[10,1482,1483],{},"However, this poses another challenge: the worker script loads from the Laravel server, but only Vite can serves it, the Laravel server isn't aware of this script and the request will just return a 404 error.",[10,1485,1486],{},"But, we have full control over the Laravel server. We can create a route to proxy requests to the Vite server, allowing the Laravel server to serve the worker script.",[10,1488,1489],{},"This seems viable, faisable and elegant enough. Let's implement it.",[10,1491,1492,1493,1496,1497,1500,1501,1504],{},"We can create a Laravel route, available only in development, to proxy requests to the Vite server. Let's craft a ",[110,1494,1495],{},"proxy.dev.php"," file within Laravel's ",[110,1498,1499],{},"routes"," directory and including it in ",[110,1502,1503],{},"routes\u002Fweb.php"," just during development.",[103,1506,1510],{"className":1507,"code":1508,"language":1509,"meta":108,"style":108},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","if (!app()->isProduction()) {\n    require __DIR__ . '\u002Fproxy.dev.php';\n}\n","php",[110,1511,1512,1517,1522],{"__ignoreMap":108},[113,1513,1514],{"class":115,"line":116},[113,1515,1516],{},"if (!app()->isProduction()) {\n",[113,1518,1519],{"class":115,"line":269},[113,1520,1521],{},"    require __DIR__ . '\u002Fproxy.dev.php';\n",[113,1523,1524],{"class":115,"line":163},[113,1525,382],{},[10,1527,1528,1529,744],{},"Next, define our proxy route in ",[110,1530,1495],{},[103,1532,1534],{"className":1507,"code":1533,"language":1509,"meta":108,"style":108},"\u003C?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('\u002Fnode_modules\u002F{any}', function ($any) {\n    $url = \"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F{$any}\";\n    return response()->stream(function () use ($url) {\n        \u002F\u002F Directly stream the remote content without loading it fully into memory\n        readfile($url);\n    }, 200, [\n      'Content-Type' => 'text\u002Fjavascript'\n      'Cache-Control' => 'public, max-age=86400' \u002F\u002F Cache for 1 day\n    ]);\n})->where('any', '(.*)');\n",[110,1535,1536,1541,1545,1550,1554,1559,1564,1569,1574,1579,1584,1589,1597,1602],{"__ignoreMap":108},[113,1537,1538],{"class":115,"line":116},[113,1539,1540],{},"\u003C?php\n",[113,1542,1543],{"class":115,"line":269},[113,1544,423],{"emptyLinePlaceholder":422},[113,1546,1547],{"class":115,"line":163},[113,1548,1549],{},"use Illuminate\\Support\\Facades\\Route;\n",[113,1551,1552],{"class":115,"line":346},[113,1553,423],{"emptyLinePlaceholder":422},[113,1555,1556],{"class":115,"line":366},[113,1557,1558],{},"Route::get('\u002Fnode_modules\u002F{any}', function ($any) {\n",[113,1560,1561],{"class":115,"line":379},[113,1562,1563],{},"    $url = \"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F{$any}\";\n",[113,1565,1566],{"class":115,"line":504},[113,1567,1568],{},"    return response()->stream(function () use ($url) {\n",[113,1570,1571],{"class":115,"line":509},[113,1572,1573],{},"        \u002F\u002F Directly stream the remote content without loading it fully into memory\n",[113,1575,1576],{"class":115,"line":925},[113,1577,1578],{},"        readfile($url);\n",[113,1580,1581],{"class":115,"line":939},[113,1582,1583],{},"    }, 200, [\n",[113,1585,1586],{"class":115,"line":947},[113,1587,1588],{},"      'Content-Type' => 'text\u002Fjavascript'\n",[113,1590,1591,1594],{"class":115,"line":959},[113,1592,1593],{},"      'Cache-Control' => 'public, max-age=86400'",[113,1595,1596],{}," \u002F\u002F Cache for 1 day\n",[113,1598,1599],{"class":115,"line":965},[113,1600,1601],{},"    ]);\n",[113,1603,1604],{"class":115,"line":971},[113,1605,1606],{},"})->where('any', '(.*)');\n",[546,1608,1609],{},[10,1610,1611,1613],{},[113,1612,552],{},"\nI do my best to optimize the proxy to efficiently stream the content without adding too much overhead. If you think of a better way to do it, feel free to share it with me in the comments.",[10,1615,1616],{},"And there you have it! The worker script loads, and the Monaco Editor operates perfectly.",[10,1618,1619],{},[85,1620],{"alt":1621,"src":1622},"Monaco Editor with a working JSON schema validation","\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Fworking-monaco-editor-worker.png",[10,1624,1625],{},"One challenge was finding a solution that works well in development, during build, and in production. This approach seems to be an elegant compromise, simple, effective, and clean. So far, it's working perfectly.",[10,1627,1628],{},"Here's an overview of the solution:",[10,1630,1631],{},[85,1632],{"alt":1633,"src":1634},"Architecture of the solution","\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Flarave-proxying-to-vite.png",[43,1636,1638],{"id":1637},"conclusion","Conclusion",[10,1640,1641],{},"This issue was indeed a challenge. Integrating the Monaco Editor with JSON schema validation was essential for our project. Unfortunately, the cross-origin headache and scant online resources forced me to create my own solution. But I found one!",[10,1643,1644,1645,1647],{},"I hope this article assists those facing similar issues. The key takeaway: mastering your tools is crucial. Although ",[20,1646,32],{}," plugins might seem daunting, understanding them removes all limitations allowing you to solve complex problems.",[1649,1650,1651],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":108,"searchDepth":269,"depth":269,"links":1653},[1654,1655,1656,1657,1658],{"id":45,"depth":269,"text":46},{"id":181,"depth":269,"text":182},{"id":562,"depth":269,"text":563},{"id":1015,"depth":269,"text":1016},{"id":1637,"depth":269,"text":1638},"0b59cb4d-f145-4011-9d8c-ef32e2770273","2025-02-06","Explore an integration problem with Laravel and Vite, focusing on cross-origin issues and solved thanks to an in-depth knowledge of tools.","md",{},"\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin","---\ncontentId: 0b59cb4d-f145-4011-9d8c-ef32e2770273\ntitle: \"Laravel and Vite: A Love Story Ruined with Cross-Origin\"\ndescription: \"Explore an integration problem with Laravel and Vite, focusing on cross-origin issues and solved thanks to an in-depth knowledge of tools.\"\ndate: 2025-02-06\n---\n\n_This title might sound dramatic, but trust me, it's worth your read. Dive in!_\n\nIt's great to be back to my blog. I've been away due to numerous conference preparations and a major internal updates to this website. Although these aren't today's topics, I'll certainly cover them in future posts. Also, this is my first article on **Laravel**, and it won't be the last!\n\nToday, I want to share an experience I had with **Laravel** and **Vite** because of the strictness of the [Web Worker API](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FUsing_web_workers).\n\n## The Laravel and Vite integration\n\nBefore delving into the problem, it's crucial to understand how **Vite** operates within a **Laravel** project.\n\nFirst, what is **Vite**? It's a fast frontend build tool that powers next-gen web applications. It allows you to construct your frontend assets (JavaScript, CSS, etc.) efficiently, especially during development. The core of Vite is a plugin pipeline that transforms source files into the required outputs, a point we'll revisit soon.\n\nAnd **Laravel**? It's a web application framework known for its expressive, elegant syntax. Laravel eases common tasks like authentication, routing, sessions, and caching, aiming to make development enjoyable without compromising functionality. _And it excels at it!_\n\nWhen you embrace **Laravel** and **Vite**, you get a formidable stack for web application development.\n\nSounds great, right? But how does it works practically? Let me illustrate.\n\n![Laravel and Vite integration](\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Flaravel-vite-request-response.png)\n\nThis image offers a high-level overview of the Laravel and Vite architecture. During development, two servers run: one for Laravel and one for Vite.\n\n1. A user requests a page from the Laravel server.\n2. The Laravel server sends the HTML page to the user. Before sending the response, Laravel adds a link tag to the entry script served by the Vite server. For example, you might find this line in the HTML returned by the Laravel server:\n\n```html\n\u003Cscript type=\"module\" src=\"http:\u002F\u002Flocalhost:5173\u002Findex.ts\">\u003C\u002Fscript>\n```\n\n3. The user's browser loads the HTML and detects the script tag pointing to the Vite server, then requests the script from the Vite server.\n4. The Vite server delivers the script to the user's browser.\n\nThis process is straightforward and highly effective during development. It gives you access to Vite's full power with the plugin pipeline and hot module replacement (HMR). _Many developers might not fully appreciate how seamlessly this integration works compared to other tools._\n\nHowever, sometimes things get tricky. And that's where our story begins.\n\n## The context\n\nFor a project, I had to use the [Monaco Editor](https:\u002F\u002Fmicrosoft.github.io\u002Fmonaco-editor\u002F), a browser-based code editor and the core of Visual Studio Code. The aim was to display a JSON in the code editor and validate it against a JSON schema. Users can edit this JSON, and the Monaco Editor, with JSON schema validation, will show in real time whether the JSON is valid or suggest corrections. Amazingly efficient when it works! _And it has been much more complexe than expected._\n\n![Monaco Editor with a familiar interface if you're using Visual Studio Code](\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Fmonaco-editor.png)\n\nTo achieve this, the Monaco Editor requires starting some Web Workers. Why and what are Web Workers?\n\nJavaScript, especially in browser environments, operates on a single thread, meaning it executes one task at a time. So, if a piece of code takes long to execute, the browser becomes unresponsive which degrades the user experience. This is why web developers advise against blocking the main thread.\n\nTo handle long tasks without affecting performance, we have two options:\n\n- **Asynchronous JavaScript**: This involves breaking down tasks into smaller chunks and executing them asynchronously. This way, the main thread remains free to handle other tasks.\n\n- **Web Workers**: For more complex tasks, especially those requiring heavy computation, we use Web Workers. A Web Worker is essentially a JavaScript file running in the background, separate from other scripts, with which it can communicate via messages.\n\nHere's a simple example of a Web Worker script:\n\n```ts [worker.js]\nglobThis.onmessage = function (e) {\n  console.log('Message received from main script')\n  const workerResult = `Result: ${e.data[0] * e.data[1]}`\n  console.log('Posting message back to main script')\n  postMessage(workerResult)\n}\n```\n\nWith its main script:\n\n```ts [main.js]\nconst worker = new Worker('worker.js')\n\nworker.addEventListener('message', (e) => {\n  console.log('Message received from worker')\n  console.log(e.data)\n})\n\nworker.postMessage([2, 3])\n```\n\nIn this example, the main script creates a new worker and sends it a message using `postMessage`. The worker receives and responds with `postMessage`. Due to JavaScript's event-driven nature, the main script listens for the worker's message using `addEventListener`.\n\nIn our case, Monaco Editor leverages Web Workers for certain features, including JSON schema validation.\n\n> [!NOTE]\n> Using a Web Worker allows the Monaco Editor to run a [LSP](https:\u002F\u002Fmicrosoft.github.io\u002Flanguage-server-protocol\u002F) server in the background for each language, providing more advanced features without impacting the main thread and the user experience.\n\n## The issue\n\nBut here's the catch:\n\nThe script passed to the Web Worker must be accessible from the same origin as the main script. This is where the problem arises.\n\n> [!WARNING]\n> This is a security feature to prevent cross-origin attacks so it's impossible to bypass it.\n\nAn origin is defined by a URL's scheme, host, and port. This means `https:\u002F\u002Fexample.com` and `https:\u002F\u002Fexample.com:1024` are considered different origins.\n\nImagine we're on the page `https:\u002F\u002Fexample.com` and you want to create a Web Worker. The script URL passed to the Web Worker must be accessible from `https:\u002F\u002Fexample.com`.\n\nThis means that this works:\n\n```html\n\u003Cscript>\n  const worker = new Worker('worker.js')\n\u003C\u002Fscript>\n```\n\nBut this doesn't:\n\n```html\n\u003Cscript>\n  const worker = new Worker('https:\u002F\u002Fanother-origin.example.com\u002Fworker.js')\n\u003C\u002Fscript>\n```\n\nDo you see the issue now?\n\nOur Laravel server runs on `http:\u002F\u002Flocalhost:8000`, while Vite is on `http:\u002F\u002Flocalhost:5173`. `http:\u002F\u002Flocalhost:8000` and `http:\u002F\u002Flocalhost:5173` are two different origins. This means the browser won't load the Web Worker script from the Vite server, resulting in a cross-origin issue due to running dual servers.\n\nIn the browser, we can easily spot this error with the following warning:\n\n```sh\napp.js:21 Failed to construct 'Worker': Script at 'http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker.js?worker_file&type=module' cannot be accessed from origin 'http:\u002F\u002Flocalhost:'.\n```\n\nThis error is a showstopper. The Monaco Editor can't function properly without the Web Worker script. Let's find a solution.\n\nSpoiler alert, I searched high and low online for a working solution. Sadly, I couldn't find one. So I try to tackle the issue myself. After extensive research and trials, I found a solution that I'm sharing it with you in this article.\n\nThe problematic code looks like this:\n\n```ts\nimport * as monaco from 'monaco-editor'\nimport EditorWorker from 'monaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker?worker'\nimport JsonWorker from 'monaco-editor\u002Fesm\u002Fvs\u002Flanguage\u002Fjson\u002Fjson.worker?worker'\n\nglobalThis.MonacoEnvironment = {\n  getWorker(workerId, label) {\n    switch (label) {\n      case 'json':\n        return new JsonWorker()\n      default:\n        return new EditorWorker()\n    }\n  },\n}\n\n\u002F\u002F Monaco Editor initialization and Diagnostic feature\n```\n\nWhen Monaco is initialized, it tries to load the Web Worker script from the Vite server. This is where the cross-origin issue arises.\n\n> [!NOTE]\n> In frameworks like [Adonis](https:\u002F\u002Fadonisjs.com\u002F), where Vite serves as middleware, this issue doesn't occur because Vite runs within the same application as the server. But in Laravel, with two separate servers, we needed a workaround.\n\n> [!NOTE]\n> This issue doesn't exist if assets are built and loaded from `\u002Fpublic\u002Fbuild`, as everything is served by the Laravel server. However, this significantly impacts the developer experience, requiring asset rebuilding and page refreshing for every change. This isn't aligned with Vite's experience or its on-demand philosophy.\n\n## The solution\n\nNow we have a clear understanding of the issue and the origin of the problem. Let's dive into the solution.\n\nFirst, we can examine the worker script URL:\n\n```txt\nmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker?worker\n```\n\nIt's a worker script, not intended for direct browser loading. Notice the `?worker` query parameter, a Vite hint to wrap the script for easy use. We can inspect the response to have a better understanding.\n\n```js\nexport default function WorkerWrapper(options) {\n  return new Worker(\n    'http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module',\n    {\n      type: 'module',\n      name: options?.name\n    }\n  )\n}\n```\n\nThanks to the `?worker` query parameter, Vite automatically wraps the script in a function that returns a new `Worker` instance. So when we import the script, we can easily invoke the function to start the worker without having to manually create a new `Worker` instance with the correct URL and options.\n\nThe worker use the following script:\n\n```txt\nhttp:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module\n```\n\nThe `http:\u002F\u002Flocalhost:5173` origin belongs to the Vite server and it's the root of the issue.\n\nTo be successful, the script URL must match our Laravel server's origin:\n\n```txt\nhttp:\u002F\u002Flocalhost:8000\u002Fnode_modules\u002F.pnpm\u002Fmonaco-editor@0.52.2\u002Fnode_modules\u002Fmonaco-editor\u002Fesm\u002Fvs\u002Feditor\u002Feditor.worker.js?worker_file&type=module\n```\n\nBut how to modify the URL dynamically to match the Laravel server origin?\n\nWith a Vite plugin! It's the core of Vite to transform source files on the fly. With a plugin, we could intercept the worker script request and adjust the URL to match the Laravel server origin.\n\nIn our `vite.config.ts`, we can define a plugin to rewrite the worker script URL:\n\n```ts\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [\n    (() => {\n      return {\n        name: 'monaco-editor:rewrite-worker',\n        transform(code, id) {\n          if (this.environment.mode !== 'dev') {\n            return\n          }\n\n          if (id.includes('worker')) {\n            return code.replace(\n              '__laravel_vite_placeholder__.test',\n              'localhost:8000',\n            )\n          }\n        },\n      }\n    })(),\n  ],\n})\n```\n\n> [!NOTE]\n> Here, we're replacing `__laravel_vite_placeholder__.test` instead of `http:\u002F\u002Flocalhost:5173`, as this URL is typically managed by the Laravel Vite plugin.\n\nWith this plugin, we can now load the Monaco Editor worker script since the origin matches the Laravel server. In production, the script URL will be correct and working as expected so we don't need to rewrite the URL. Build files are served from the same origin using a single server like Nginx, Apache or Caddy.\n\nHowever, this poses another challenge: the worker script loads from the Laravel server, but only Vite can serves it, the Laravel server isn't aware of this script and the request will just return a 404 error.\n\nBut, we have full control over the Laravel server. We can create a route to proxy requests to the Vite server, allowing the Laravel server to serve the worker script.\n\nThis seems viable, faisable and elegant enough. Let's implement it.\n\nWe can create a Laravel route, available only in development, to proxy requests to the Vite server. Let's craft a `proxy.dev.php` file within Laravel's `routes` directory and including it in `routes\u002Fweb.php` just during development.\n\n```php\nif (!app()->isProduction()) {\n    require __DIR__ . '\u002Fproxy.dev.php';\n}\n```\n\nNext, define our proxy route in `proxy.dev.php`:\n\n```php\n\u003C?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('\u002Fnode_modules\u002F{any}', function ($any) {\n    $url = \"http:\u002F\u002Flocalhost:5173\u002Fnode_modules\u002F{$any}\";\n    return response()->stream(function () use ($url) {\n        \u002F\u002F Directly stream the remote content without loading it fully into memory\n        readfile($url);\n    }, 200, [\n      'Content-Type' => 'text\u002Fjavascript'\n      'Cache-Control' => 'public, max-age=86400' \u002F\u002F Cache for 1 day\n    ]);\n})->where('any', '(.*)');\n```\n\n> [!NOTE]\n> I do my best to optimize the proxy to efficiently stream the content without adding too much overhead. If you think of a better way to do it, feel free to share it with me in the comments.\n\nAnd there you have it! The worker script loads, and the Monaco Editor operates perfectly.\n\n![Monaco Editor with a working JSON schema validation](\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Fworking-monaco-editor-worker.png)\n\nOne challenge was finding a solution that works well in development, during build, and in production. This approach seems to be an elegant compromise, simple, effective, and clean. So far, it's working perfectly.\n\nHere's an overview of the solution:\n\n![Architecture of the solution](\u002Fposts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin\u002Flarave-proxying-to-vite.png)\n\n## Conclusion\n\nThis issue was indeed a challenge. Integrating the Monaco Editor with JSON schema validation was essential for our project. Unfortunately, the cross-origin headache and scant online resources forced me to create my own solution. But I found one!\n\nI hope this article assists those facing similar issues. The key takeaway: mastering your tools is crucial. Although **Vite** plugins might seem daunting, understanding them removes all limitations allowing you to solve complex problems.\n",null,{"title":5,"description":1661},{"loc":1664,"images":1669},[1670,1671,1672,1673],{"loc":88},{"loc":200},{"loc":1622},{"loc":1634},"posts\u002Flaravel-and-vite-a-love-story-ruined-with-cross-origin","nM8grVfBoNFFJB-CQTGvsDXUu5Nqw11xVi5xyEQwKPg",1790086900911]