@@ -2,6 +2,7 @@ import type * as vite from 'vite';
2
2
import {
3
3
BuildStepOutput ,
4
4
Entrypoint ,
5
+ EntrypointGroup ,
5
6
ResolvedConfig ,
6
7
WxtBuilder ,
7
8
WxtBuilderServer ,
@@ -24,7 +25,9 @@ import { ViteNodeServer } from 'vite-node/server';
24
25
import { ViteNodeRunner } from 'vite-node/client' ;
25
26
import { installSourcemapsSupport } from 'vite-node/source-map' ;
26
27
import { createExtensionEnvironment } from '../../utils/environments' ;
27
- import { relative } from 'node:path' ;
28
+ import { relative , join , extname , dirname } from 'node:path' ;
29
+ import fs from 'fs-extra' ;
30
+ import { normalizePath } from '../../utils/paths' ;
28
31
29
32
export async function createViteBuilder (
30
33
wxtConfig : ResolvedConfig ,
@@ -172,10 +175,7 @@ export async function createViteBuilder(
172
175
) ;
173
176
return {
174
177
mode : wxtConfig . mode ,
175
- plugins : [
176
- wxtPlugins . multipageMove ( entrypoints , wxtConfig ) ,
177
- wxtPlugins . entrypointGroupGlobals ( entrypoints ) ,
178
- ] ,
178
+ plugins : [ wxtPlugins . entrypointGroupGlobals ( entrypoints ) ] ,
179
179
build : {
180
180
rollupOptions : {
181
181
input : entrypoints . reduce < Record < string , string > > ( ( input , entry ) => {
@@ -319,9 +319,10 @@ export async function createViteBuilder(
319
319
buildConfig ,
320
320
) ;
321
321
const result = await vite . build ( buildConfig ) ;
322
+ const chunks = getBuildOutputChunks ( result ) ;
322
323
return {
323
324
entrypoints : group ,
324
- chunks : getBuildOutputChunks ( result ) ,
325
+ chunks : await moveHtmlFiles ( wxtConfig , group , chunks ) ,
325
326
} ;
326
327
} ,
327
328
async createServer ( info ) {
@@ -400,3 +401,76 @@ function getRollupEntry(entrypoint: Entrypoint): string {
400
401
}
401
402
return entrypoint . inputPath ;
402
403
}
404
+
405
+ /**
406
+ * Ensures the HTML files output by a multipage build are in the correct location. This does two
407
+ * things:
408
+ *
409
+ * 1. Moves the HTML files to their final location at `<outDir>/<entrypoint.name>.html`.
410
+ * 2. Updates the bundle so it summarizes the files correctly in the returned build output.
411
+ *
412
+ * Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't need to be modified.
413
+ * HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML
414
+ * files either.
415
+ */
416
+ async function moveHtmlFiles (
417
+ config : ResolvedConfig ,
418
+ group : EntrypointGroup ,
419
+ chunks : BuildStepOutput [ 'chunks' ] ,
420
+ ) : Promise < BuildStepOutput [ 'chunks' ] > {
421
+ if ( ! Array . isArray ( group ) ) return chunks ;
422
+
423
+ const entryMap = group . reduce < Record < string , Entrypoint > > ( ( map , entry ) => {
424
+ const a = normalizePath ( relative ( config . root , entry . inputPath ) ) ;
425
+ map [ a ] = entry ;
426
+ return map ;
427
+ } , { } ) ;
428
+
429
+ const movedChunks = await Promise . all (
430
+ chunks . map ( async ( chunk ) => {
431
+ if ( ! chunk . fileName . endsWith ( '.html' ) ) return chunk ;
432
+
433
+ const entry = entryMap [ chunk . fileName ] ;
434
+ const oldBundlePath = chunk . fileName ;
435
+ const newBundlePath = getEntrypointBundlePath (
436
+ entry ,
437
+ config . outDir ,
438
+ extname ( chunk . fileName ) ,
439
+ ) ;
440
+ const oldAbsPath = join ( config . outDir , oldBundlePath ) ;
441
+ const newAbsPath = join ( config . outDir , newBundlePath ) ;
442
+ await fs . ensureDir ( dirname ( newAbsPath ) ) ;
443
+ await fs . move ( oldAbsPath , newAbsPath , { overwrite : true } ) ;
444
+
445
+ return {
446
+ ...chunk ,
447
+ fileName : newBundlePath ,
448
+ } ;
449
+ } ) ,
450
+ ) ;
451
+
452
+ // TODO: Optimize and only delete old path directories
453
+ removeEmptyDirs ( config . outDir ) ;
454
+
455
+ return movedChunks ;
456
+ }
457
+
458
+ /**
459
+ * Recursively remove all directories that are empty/
460
+ */
461
+ export async function removeEmptyDirs ( dir : string ) : Promise < void > {
462
+ const files = await fs . readdir ( dir ) ;
463
+ for ( const file of files ) {
464
+ const filePath = join ( dir , file ) ;
465
+ const stats = await fs . stat ( filePath ) ;
466
+ if ( stats . isDirectory ( ) ) {
467
+ await removeEmptyDirs ( filePath ) ;
468
+ }
469
+ }
470
+
471
+ try {
472
+ await fs . rmdir ( dir ) ;
473
+ } catch {
474
+ // noop on failure - this means the directory was not empty.
475
+ }
476
+ }
0 commit comments