wxt 0.17.8 → 0.17.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import cac from "cac";
8
8
 
9
9
  // src/core/utils/fs.ts
10
- import fs from "fs-extra";
10
+ import fs3 from "fs-extra";
11
11
  import glob from "fast-glob";
12
12
 
13
13
  // src/core/utils/paths.ts
@@ -230,206 +230,15 @@ var packageManagers = {
230
230
  yarn
231
231
  };
232
232
 
233
- // src/core/wxt.ts
234
- var wxt;
235
- async function registerWxt(command, inlineConfig = {}, server) {
236
- const config = await resolveConfig(inlineConfig, command, server);
237
- const hooks = createHooks();
238
- const pm = await createWxtPackageManager(config.root);
239
- wxt = {
240
- config,
241
- hooks,
242
- get logger() {
243
- return config.logger;
244
- },
245
- async reloadConfig() {
246
- wxt.config = await resolveConfig(inlineConfig, command, server);
247
- },
248
- pm
249
- };
250
- wxt.hooks.addHooks(config.hooks);
251
- await wxt.hooks.callHook("ready", wxt);
252
- }
253
-
254
- // src/core/utils/fs.ts
255
- async function writeFileIfDifferent(file, newContents) {
256
- const existingContents = await fs.readFile(file, "utf-8").catch(() => void 0);
257
- if (existingContents !== newContents) {
258
- await fs.writeFile(file, newContents);
259
- }
260
- }
261
- async function getPublicFiles() {
262
- if (!await fs.exists(wxt.config.publicDir))
263
- return [];
264
- const files = await glob("**/*", { cwd: wxt.config.publicDir });
265
- return files.map(unnormalizePath);
266
- }
267
-
268
- // src/core/utils/building/build-entrypoints.ts
269
- import fs2 from "fs-extra";
270
- import { dirname, resolve } from "path";
271
- import pc from "picocolors";
272
- async function buildEntrypoints(groups, spinner) {
273
- const steps = [];
274
- for (let i = 0; i < groups.length; i++) {
275
- const group = groups[i];
276
- const groupNames = [group].flat().map((e) => e.name);
277
- const groupNameColored = groupNames.join(pc.dim(", "));
278
- spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
279
- try {
280
- steps.push(await wxt.config.builder.build(group));
281
- } catch (err) {
282
- spinner.stop().clear();
283
- wxt.logger.error(err);
284
- throw Error(`Failed to build ${groupNames.join(", ")}`, { cause: err });
285
- }
286
- }
287
- const publicAssets = await copyPublicDirectory();
288
- return { publicAssets, steps };
289
- }
290
- async function copyPublicDirectory() {
291
- const files = await getPublicFiles();
292
- if (files.length === 0)
293
- return [];
294
- const publicAssets = [];
295
- for (const file of files) {
296
- const srcPath = resolve(wxt.config.publicDir, file);
297
- const outPath = resolve(wxt.config.outDir, file);
298
- await fs2.ensureDir(dirname(outPath));
299
- await fs2.copyFile(srcPath, outPath);
300
- publicAssets.push({
301
- type: "asset",
302
- fileName: file
303
- });
304
- }
305
- return publicAssets;
306
- }
307
-
308
- // src/core/utils/arrays.ts
309
- function every(array, predicate) {
310
- for (let i = 0; i < array.length; i++)
311
- if (!predicate(array[i], i))
312
- return false;
313
- return true;
314
- }
315
- function some(array, predicate) {
316
- for (let i = 0; i < array.length; i++)
317
- if (predicate(array[i], i))
318
- return true;
319
- return false;
320
- }
321
-
322
- // src/core/utils/building/detect-dev-changes.ts
323
- function detectDevChanges(changedFiles, currentOutput) {
324
- const isConfigChange = some(
325
- changedFiles,
326
- (file) => file === wxt.config.userConfigMetadata.configFile
327
- );
328
- if (isConfigChange)
329
- return { type: "full-restart" };
330
- const isRunnerChange = some(
331
- changedFiles,
332
- (file) => file === wxt.config.runnerConfig.configFile
333
- );
334
- if (isRunnerChange)
335
- return { type: "browser-restart" };
336
- const changedSteps = new Set(
337
- changedFiles.flatMap(
338
- (changedFile) => findEffectedSteps(changedFile, currentOutput)
339
- )
340
- );
341
- if (changedSteps.size === 0)
342
- return { type: "no-change" };
343
- const unchangedOutput = {
344
- manifest: currentOutput.manifest,
345
- steps: [],
346
- publicAssets: []
347
- };
348
- const changedOutput = {
349
- manifest: currentOutput.manifest,
350
- steps: [],
351
- publicAssets: []
352
- };
353
- for (const step of currentOutput.steps) {
354
- if (changedSteps.has(step)) {
355
- changedOutput.steps.push(step);
356
- } else {
357
- unchangedOutput.steps.push(step);
358
- }
359
- }
360
- for (const asset of currentOutput.publicAssets) {
361
- if (changedSteps.has(asset)) {
362
- changedOutput.publicAssets.push(asset);
363
- } else {
364
- unchangedOutput.publicAssets.push(asset);
365
- }
366
- }
367
- const isOnlyHtmlChanges = changedFiles.length > 0 && every(changedFiles, (file) => file.endsWith(".html"));
368
- if (isOnlyHtmlChanges) {
369
- return {
370
- type: "html-reload",
371
- cachedOutput: unchangedOutput,
372
- rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
373
- };
374
- }
375
- const isOnlyContentScripts = changedOutput.steps.length > 0 && every(
376
- changedOutput.steps.flatMap((step) => step.entrypoints),
377
- (entry) => entry.type === "content-script"
378
- );
379
- if (isOnlyContentScripts) {
380
- return {
381
- type: "content-script-reload",
382
- cachedOutput: unchangedOutput,
383
- changedSteps: changedOutput.steps,
384
- rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
385
- };
386
- }
387
- return {
388
- type: "extension-reload",
389
- cachedOutput: unchangedOutput,
390
- rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
391
- };
392
- }
393
- function findEffectedSteps(changedFile, currentOutput) {
394
- const changes = [];
395
- const changedPath = normalizePath(changedFile);
396
- const isChunkEffected = (chunk) => (
397
- // If it's an HTML file with the same path, is is effected because HTML files need to be re-rendered
398
- // - fileName is normalized, relative bundle path, "<entrypoint-name>.html"
399
- chunk.type === "asset" && changedPath.replace("/index.html", ".html").endsWith(chunk.fileName) || // If it's a chunk that depends on the changed file, it is effected
400
- // - moduleIds are absolute, normalized paths
401
- chunk.type === "chunk" && chunk.moduleIds.includes(changedPath)
402
- );
403
- for (const step of currentOutput.steps) {
404
- const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
405
- if (effectedChunk)
406
- changes.push(step);
407
- }
408
- const effectedAsset = currentOutput.publicAssets.find(
409
- (chunk) => isChunkEffected(chunk)
410
- );
411
- if (effectedAsset)
412
- changes.push(effectedAsset);
413
- return changes;
414
- }
415
-
416
- // src/core/utils/building/find-entrypoints.ts
417
- import { relative as relative2, resolve as resolve3 } from "path";
418
- import fs3 from "fs-extra";
419
- import { minimatch } from "minimatch";
420
- import { parseHTML } from "linkedom";
421
- import JSON5 from "json5";
422
- import glob2 from "fast-glob";
423
-
424
233
  // src/core/utils/entrypoints.ts
425
- import path2, { relative, resolve as resolve2 } from "node:path";
234
+ import path2, { relative, resolve } from "node:path";
426
235
  function getEntrypointName(entrypointsDir, inputPath) {
427
236
  const relativePath = path2.relative(entrypointsDir, inputPath);
428
237
  const name = relativePath.split(/[\.\/\\]/, 2)[0];
429
238
  return name;
430
239
  }
431
240
  function getEntrypointOutputFile(entrypoint, ext) {
432
- return resolve2(entrypoint.outputDir, `${entrypoint.name}${ext}`);
241
+ return resolve(entrypoint.outputDir, `${entrypoint.name}${ext}`);
433
242
  }
434
243
  function getEntrypointBundlePath(entrypoint, outDir, ext) {
435
244
  return normalizePath(
@@ -453,386 +262,445 @@ function isHtmlEntrypoint(entrypoint) {
453
262
  return entrypoint.inputPath.endsWith(".html");
454
263
  }
455
264
 
456
- // src/core/utils/constants.ts
457
- var VIRTUAL_NOOP_BACKGROUND_MODULE_ID = "virtual:user-background";
458
-
459
- // src/core/utils/building/find-entrypoints.ts
460
- import pc2 from "picocolors";
461
- async function findEntrypoints() {
462
- const relativePaths = await glob2(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
463
- cwd: wxt.config.entrypointsDir
464
- });
465
- relativePaths.sort();
466
- const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
467
- const entrypointInfos = relativePaths.reduce((results, relativePath) => {
468
- const inputPath = resolve3(wxt.config.entrypointsDir, relativePath);
469
- const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
470
- const matchingGlob = pathGlobs.find(
471
- (glob6) => minimatch(relativePath, glob6)
472
- );
473
- if (matchingGlob) {
474
- const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
475
- results.push({
476
- name,
477
- inputPath,
478
- type,
479
- skipped: wxt.config.filterEntrypoints != null && !wxt.config.filterEntrypoints.has(name)
480
- });
481
- }
482
- return results;
483
- }, []);
484
- preventNoEntrypoints(entrypointInfos);
485
- preventDuplicateEntrypointNames(entrypointInfos);
486
- let hasBackground = false;
487
- const entrypoints = await Promise.all(
488
- entrypointInfos.map(async (info) => {
489
- const { type } = info;
490
- switch (type) {
491
- case "popup":
492
- return await getPopupEntrypoint(info);
493
- case "sidepanel":
494
- return await getSidepanelEntrypoint(info);
495
- case "options":
496
- return await getOptionsEntrypoint(info);
497
- case "background":
498
- hasBackground = true;
499
- return await getBackgroundEntrypoint(info);
500
- case "content-script":
501
- return await getContentScriptEntrypoint(info);
502
- case "unlisted-page":
503
- return await getUnlistedPageEntrypoint(info);
504
- case "unlisted-script":
505
- return await getUnlistedScriptEntrypoint(info);
506
- case "content-script-style":
507
- return {
508
- ...info,
509
- type,
510
- outputDir: resolve3(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
511
- options: {
512
- include: void 0,
513
- exclude: void 0
514
- }
515
- };
516
- default:
517
- return {
518
- ...info,
519
- type,
520
- outputDir: wxt.config.outDir,
521
- options: {
522
- include: void 0,
523
- exclude: void 0
524
- }
525
- };
526
- }
527
- })
265
+ // src/core/builders/vite/plugins/devHtmlPrerender.ts
266
+ import { parseHTML } from "linkedom";
267
+ import { dirname, relative as relative2, resolve as resolve2 } from "node:path";
268
+ var reactRefreshPreamble = "";
269
+ function devHtmlPrerender(config, server) {
270
+ const htmlReloadId = "@wxt/reload-html";
271
+ const resolvedHtmlReloadId = resolve2(
272
+ config.wxtModuleDir,
273
+ "dist/virtual/reload-html.js"
528
274
  );
529
- if (wxt.config.command === "serve" && !hasBackground) {
530
- entrypoints.push(
531
- await getBackgroundEntrypoint({
532
- inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
533
- name: "background",
534
- type: "background",
535
- skipped: false
536
- })
537
- );
538
- }
539
- wxt.logger.debug("All entrypoints:", entrypoints);
540
- const skippedEntrypointNames = entrypointInfos.filter((item) => item.skipped).map((item) => item.name);
541
- if (skippedEntrypointNames.length) {
542
- wxt.logger.warn(
543
- `Filter excluded the following entrypoints:
544
- ${skippedEntrypointNames.map((item) => `${pc2.dim("-")} ${pc2.cyan(item)}`).join("\n")}`
275
+ const virtualReactRefreshId = "@wxt/virtual-react-refresh";
276
+ const resolvedVirtualReactRefreshId = "\0" + virtualReactRefreshId;
277
+ return [
278
+ {
279
+ apply: "build",
280
+ name: "wxt:dev-html-prerender",
281
+ config() {
282
+ return {
283
+ resolve: {
284
+ alias: {
285
+ [htmlReloadId]: resolvedHtmlReloadId
286
+ }
287
+ }
288
+ };
289
+ },
290
+ // Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
291
+ // before the paths are replaced with their bundled path
292
+ transform(code, id) {
293
+ if (config.command !== "serve" || server == null || !id.endsWith(".html"))
294
+ return;
295
+ const { document } = parseHTML(code);
296
+ const _pointToDevServer = (querySelector, attr) => pointToDevServer(config, server, id, document, querySelector, attr);
297
+ _pointToDevServer("script[type=module]", "src");
298
+ _pointToDevServer("link[rel=stylesheet]", "href");
299
+ const reloader = document.createElement("script");
300
+ reloader.src = htmlReloadId;
301
+ reloader.type = "module";
302
+ document.head.appendChild(reloader);
303
+ const newHtml = document.toString();
304
+ config.logger.debug("transform " + id);
305
+ config.logger.debug("Old HTML:\n" + code);
306
+ config.logger.debug("New HTML:\n" + newHtml);
307
+ return newHtml;
308
+ },
309
+ // Pass the HTML through the dev server to add dev-mode specific code
310
+ async transformIndexHtml(html, ctx) {
311
+ if (config.command !== "serve" || server == null)
312
+ return;
313
+ const originalUrl = `${server.origin}${ctx.path}`;
314
+ const name = getEntrypointName(config.entrypointsDir, ctx.filename);
315
+ const url = `${server.origin}/${name}.html`;
316
+ const serverHtml = await server.transformHtml(url, html, originalUrl);
317
+ const { document } = parseHTML(serverHtml);
318
+ const reactRefreshScript = Array.from(
319
+ document.querySelectorAll("script[type=module]")
320
+ ).find((script) => script.innerHTML.includes("@react-refresh"));
321
+ if (reactRefreshScript) {
322
+ reactRefreshPreamble = reactRefreshScript.innerHTML;
323
+ const virtualScript = document.createElement("script");
324
+ virtualScript.type = "module";
325
+ virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
326
+ reactRefreshScript.replaceWith(virtualScript);
327
+ }
328
+ const viteClientScript = document.querySelector(
329
+ "script[src='/@vite/client']"
330
+ );
331
+ if (viteClientScript) {
332
+ viteClientScript.src = `${server.origin}${viteClientScript.src}`;
333
+ }
334
+ const newHtml = document.toString();
335
+ config.logger.debug("transformIndexHtml " + ctx.filename);
336
+ config.logger.debug("Old HTML:\n" + html);
337
+ config.logger.debug("New HTML:\n" + newHtml);
338
+ return newHtml;
339
+ }
340
+ },
341
+ {
342
+ name: "wxt:virtualize-react-refresh",
343
+ apply: "serve",
344
+ resolveId(id) {
345
+ if (id === `/${virtualReactRefreshId}`) {
346
+ return resolvedVirtualReactRefreshId;
347
+ }
348
+ if (id.startsWith("/chunks/")) {
349
+ return "\0noop";
350
+ }
351
+ },
352
+ load(id) {
353
+ if (id === resolvedVirtualReactRefreshId) {
354
+ return reactRefreshPreamble;
355
+ }
356
+ if (id === "\0noop") {
357
+ return "";
358
+ }
359
+ }
360
+ }
361
+ ];
362
+ }
363
+ function pointToDevServer(config, server, id, document, querySelector, attr) {
364
+ document.querySelectorAll(querySelector).forEach((element) => {
365
+ const src = element.getAttribute(attr);
366
+ if (!src || isUrl(src))
367
+ return;
368
+ let resolvedAbsolutePath;
369
+ const matchingAlias = Object.entries(config.alias).find(
370
+ ([key]) => src.startsWith(key)
545
371
  );
546
- }
547
- const targetEntrypoints = entrypoints.filter((entry) => {
548
- const { include, exclude } = entry.options;
549
- if (include?.length && exclude?.length) {
550
- wxt.logger.warn(
551
- `The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`
372
+ if (matchingAlias) {
373
+ const [alias, replacement] = matchingAlias;
374
+ resolvedAbsolutePath = resolve2(
375
+ config.root,
376
+ src.replace(alias, replacement)
552
377
  );
553
- return false;
554
- }
555
- if (exclude?.length && !include?.length) {
556
- return !exclude.includes(wxt.config.browser);
557
- }
558
- if (include?.length && !exclude?.length) {
559
- return include.includes(wxt.config.browser);
378
+ } else {
379
+ resolvedAbsolutePath = resolve2(dirname(id), src);
560
380
  }
561
- if (skippedEntrypointNames.includes(entry.name)) {
562
- return false;
381
+ if (resolvedAbsolutePath) {
382
+ const relativePath = normalizePath(
383
+ relative2(config.root, resolvedAbsolutePath)
384
+ );
385
+ if (relativePath.startsWith(".")) {
386
+ let path10 = normalizePath(resolvedAbsolutePath);
387
+ if (!path10.startsWith("/"))
388
+ path10 = "/" + path10;
389
+ element.setAttribute(attr, `${server.origin}/@fs${path10}`);
390
+ } else {
391
+ const url = new URL(relativePath, server.origin);
392
+ element.setAttribute(attr, url.href);
393
+ }
563
394
  }
564
- return true;
565
395
  });
566
- wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
567
- await wxt.hooks.callHook("entrypoints:resolved", wxt, targetEntrypoints);
568
- return targetEntrypoints;
569
- }
570
- function preventDuplicateEntrypointNames(files) {
571
- const namesToPaths = files.reduce(
572
- (map, { name, inputPath }) => {
573
- map[name] ??= [];
574
- map[name].push(inputPath);
575
- return map;
576
- },
577
- {}
578
- );
579
- const errorLines = Object.entries(namesToPaths).reduce(
580
- (lines, [name, absolutePaths]) => {
581
- if (absolutePaths.length > 1) {
582
- lines.push(`- ${name}`);
583
- absolutePaths.forEach((absolutePath) => {
584
- lines.push(` - ${relative2(wxt.config.root, absolutePath)}`);
585
- });
586
- }
587
- return lines;
588
- },
589
- []
590
- );
591
- if (errorLines.length > 0) {
592
- const errorContent = errorLines.join("\n");
593
- throw Error(
594
- `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.
595
-
596
- ${errorContent}`
597
- );
598
- }
599
396
  }
600
- function preventNoEntrypoints(files) {
601
- if (files.length === 0) {
602
- throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
397
+ function isUrl(str) {
398
+ try {
399
+ new URL(str);
400
+ return true;
401
+ } catch {
402
+ return false;
603
403
  }
604
404
  }
605
- async function getPopupEntrypoint(info) {
606
- const options = await getHtmlEntrypointOptions(
607
- info,
608
- {
609
- browserStyle: "browse_style",
610
- exclude: "exclude",
611
- include: "include",
612
- defaultIcon: "default_icon",
613
- defaultTitle: "default_title",
614
- mv2Key: "type"
615
- },
616
- {
617
- defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
618
- },
619
- {
620
- defaultTitle: (content) => content,
621
- mv2Key: (content) => content === "page_action" ? "page_action" : "browser_action"
622
- }
623
- );
405
+
406
+ // src/core/builders/vite/plugins/devServerGlobals.ts
407
+ function devServerGlobals(config, server) {
624
408
  return {
625
- type: "popup",
626
- name: "popup",
627
- options: resolvePerBrowserOptions(options, wxt.config.browser),
628
- inputPath: info.inputPath,
629
- outputDir: wxt.config.outDir,
630
- skipped: info.skipped
631
- };
632
- }
633
- async function getOptionsEntrypoint(info) {
634
- const options = await getHtmlEntrypointOptions(
635
- info,
636
- {
637
- browserStyle: "browse_style",
638
- chromeStyle: "chrome_style",
639
- exclude: "exclude",
640
- include: "include",
641
- openInTab: "open_in_tab"
409
+ name: "wxt:dev-server-globals",
410
+ config() {
411
+ if (server == null || config.command == "build")
412
+ return;
413
+ return {
414
+ define: {
415
+ __DEV_SERVER_PROTOCOL__: JSON.stringify("ws:"),
416
+ __DEV_SERVER_HOSTNAME__: JSON.stringify(server.hostname),
417
+ __DEV_SERVER_PORT__: JSON.stringify(server.port)
418
+ }
419
+ };
642
420
  }
643
- );
644
- return {
645
- type: "options",
646
- name: "options",
647
- options: resolvePerBrowserOptions(options, wxt.config.browser),
648
- inputPath: info.inputPath,
649
- outputDir: wxt.config.outDir,
650
- skipped: info.skipped
651
- };
652
- }
653
- async function getUnlistedPageEntrypoint(info) {
654
- const options = await getHtmlEntrypointOptions(info, {
655
- exclude: "exclude",
656
- include: "include"
657
- });
658
- return {
659
- type: "unlisted-page",
660
- name: info.name,
661
- inputPath: info.inputPath,
662
- outputDir: wxt.config.outDir,
663
- options,
664
- skipped: info.skipped
665
421
  };
666
422
  }
667
- async function getUnlistedScriptEntrypoint({
668
- inputPath,
669
- name,
670
- skipped
671
- }) {
672
- const defaultExport = await importEntrypointFile(inputPath);
673
- if (defaultExport == null) {
674
- throw Error(
675
- `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`
676
- );
677
- }
678
- const { main: _, ...options } = defaultExport;
679
- return {
680
- type: "unlisted-script",
681
- name,
682
- inputPath,
683
- outputDir: wxt.config.outDir,
684
- options: resolvePerBrowserOptions(options, wxt.config.browser),
685
- skipped
686
- };
423
+
424
+ // src/core/utils/network.ts
425
+ import dns from "node:dns";
426
+
427
+ // src/core/utils/time.ts
428
+ function formatDuration(duration) {
429
+ if (duration < 1e3)
430
+ return `${duration} ms`;
431
+ if (duration < 1e4)
432
+ return `${(duration / 1e3).toFixed(3)} s`;
433
+ if (duration < 6e4)
434
+ return `${(duration / 1e3).toFixed(1)} s`;
435
+ return `${(duration / 1e3).toFixed(0)} s`;
687
436
  }
688
- async function getBackgroundEntrypoint({
689
- inputPath,
690
- name,
691
- skipped
692
- }) {
693
- let options = {};
694
- if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
695
- const defaultExport = await importEntrypointFile(inputPath);
696
- if (defaultExport == null) {
697
- throw Error(
698
- `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`
437
+ function withTimeout(promise, duration) {
438
+ return new Promise((res, rej) => {
439
+ const timeout = setTimeout(() => {
440
+ rej(`Promise timed out after ${duration}ms`);
441
+ }, duration);
442
+ promise.then(res).catch(rej).finally(() => clearTimeout(timeout));
443
+ });
444
+ }
445
+
446
+ // src/core/utils/network.ts
447
+ function isOffline() {
448
+ const isOffline2 = new Promise((res) => {
449
+ dns.resolve("google.com", (err) => {
450
+ if (err == null) {
451
+ res(false);
452
+ } else {
453
+ res(true);
454
+ }
455
+ });
456
+ });
457
+ return withTimeout(isOffline2, 1e3).catch(() => true);
458
+ }
459
+ async function isOnline() {
460
+ const offline = await isOffline();
461
+ return !offline;
462
+ }
463
+ async function fetchCached(url, config) {
464
+ let content = "";
465
+ if (await isOnline()) {
466
+ const res = await fetch(url);
467
+ if (res.status < 300) {
468
+ content = await res.text();
469
+ await config.fsCache.set(url, content);
470
+ } else {
471
+ config.logger.debug(
472
+ `Failed to download "${url}", falling back to cache...`
699
473
  );
700
474
  }
701
- const { main: _, ...moduleOptions } = defaultExport;
702
- options = moduleOptions;
703
475
  }
704
- if (wxt.config.manifestVersion !== 3) {
705
- delete options.type;
706
- }
707
- return {
708
- type: "background",
709
- name,
710
- inputPath,
711
- outputDir: wxt.config.outDir,
712
- options: resolvePerBrowserOptions(options, wxt.config.browser),
713
- skipped
714
- };
715
- }
716
- async function getContentScriptEntrypoint({
717
- inputPath,
718
- name,
719
- skipped
720
- }) {
721
- const { main: _, ...options } = await importEntrypointFile(inputPath);
722
- if (options == null) {
476
+ if (!content)
477
+ content = await config.fsCache.get(url) ?? "";
478
+ if (!content)
723
479
  throw Error(
724
- `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`
480
+ `Offline and "${url}" has not been cached. Try again when online.`
725
481
  );
726
- }
727
- return {
728
- type: "content-script",
729
- name,
730
- inputPath,
731
- outputDir: resolve3(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
732
- options: resolvePerBrowserOptions(options, wxt.config.browser),
733
- skipped
734
- };
482
+ return content;
735
483
  }
736
- async function getSidepanelEntrypoint(info) {
737
- const options = await getHtmlEntrypointOptions(
738
- info,
739
- {
740
- browserStyle: "browse_style",
741
- exclude: "exclude",
742
- include: "include",
743
- defaultIcon: "default_icon",
744
- defaultTitle: "default_title",
745
- openAtInstall: "open_at_install"
746
- },
747
- {
748
- defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
484
+
485
+ // src/core/builders/vite/plugins/download.ts
486
+ function download(config) {
487
+ return {
488
+ name: "wxt:download",
489
+ resolveId(id) {
490
+ if (id.startsWith("url:"))
491
+ return "\0" + id;
749
492
  },
750
- {
751
- defaultTitle: (content) => content
493
+ async load(id) {
494
+ if (!id.startsWith("\0url:"))
495
+ return;
496
+ const url = id.replace("\0url:", "");
497
+ return await fetchCached(url, config);
752
498
  }
753
- );
754
- return {
755
- type: "sidepanel",
756
- name: info.name,
757
- options: resolvePerBrowserOptions(options, wxt.config.browser),
758
- inputPath: info.inputPath,
759
- outputDir: wxt.config.outDir,
760
- skipped: info.skipped
761
499
  };
762
500
  }
763
- async function getHtmlEntrypointOptions(info, keyMap, queries, parsers) {
764
- const content = await fs3.readFile(info.inputPath, "utf-8");
765
- const { document } = parseHTML(content);
766
- const options = {};
767
- const defaultQuery = (manifestKey) => document.querySelector(`meta[name='manifest.${manifestKey}']`)?.getAttribute("content");
768
- Object.entries(keyMap).forEach(([_key, manifestKey]) => {
769
- const key = _key;
770
- const content2 = queries?.[key] ? queries[key](document, manifestKey) : defaultQuery(manifestKey);
771
- if (content2) {
772
- try {
773
- options[key] = (parsers?.[key] ?? JSON5.parse)(content2);
774
- } catch (err) {
775
- wxt.logger.fatal(
776
- `Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content2})`,
777
- err
501
+
502
+ // src/core/builders/vite/plugins/multipageMove.ts
503
+ import { dirname as dirname2, extname, resolve as resolve3, join } from "node:path";
504
+ import fs, { ensureDir as ensureDir2 } from "fs-extra";
505
+ function multipageMove(entrypoints, config) {
506
+ return {
507
+ name: "wxt:multipage-move",
508
+ async writeBundle(_, bundle) {
509
+ for (const oldBundlePath in bundle) {
510
+ const entrypoint = entrypoints.find(
511
+ (entry) => !!normalizePath(entry.inputPath).endsWith(oldBundlePath)
512
+ );
513
+ if (entrypoint == null) {
514
+ config.logger.debug(
515
+ `No entrypoint found for ${oldBundlePath}, leaving in chunks directory`
516
+ );
517
+ continue;
518
+ }
519
+ const newBundlePath = getEntrypointBundlePath(
520
+ entrypoint,
521
+ config.outDir,
522
+ extname(oldBundlePath)
778
523
  );
524
+ if (newBundlePath === oldBundlePath) {
525
+ config.logger.debug(
526
+ "HTML file is already in the correct location",
527
+ oldBundlePath
528
+ );
529
+ continue;
530
+ }
531
+ const oldAbsPath = resolve3(config.outDir, oldBundlePath);
532
+ const newAbsPath = resolve3(config.outDir, newBundlePath);
533
+ await ensureDir2(dirname2(newAbsPath));
534
+ await fs.move(oldAbsPath, newAbsPath, { overwrite: true });
535
+ const renamedChunk = {
536
+ ...bundle[oldBundlePath],
537
+ fileName: newBundlePath
538
+ };
539
+ delete bundle[oldBundlePath];
540
+ bundle[newBundlePath] = renamedChunk;
779
541
  }
542
+ removeEmptyDirs(config.outDir);
780
543
  }
781
- });
782
- return options;
544
+ };
545
+ }
546
+ async function removeEmptyDirs(dir) {
547
+ const files = await fs.readdir(dir);
548
+ for (const file of files) {
549
+ const filePath = join(dir, file);
550
+ const stats = await fs.stat(filePath);
551
+ if (stats.isDirectory()) {
552
+ await removeEmptyDirs(filePath);
553
+ }
554
+ }
555
+ try {
556
+ await fs.rmdir(dir);
557
+ } catch {
558
+ }
783
559
  }
784
- var PATH_GLOB_TO_TYPE_MAP = {
785
- "sandbox.html": "sandbox",
786
- "sandbox/index.html": "sandbox",
787
- "*.sandbox.html": "sandbox",
788
- "*.sandbox/index.html": "sandbox",
789
- "bookmarks.html": "bookmarks",
790
- "bookmarks/index.html": "bookmarks",
791
- "history.html": "history",
792
- "history/index.html": "history",
793
- "newtab.html": "newtab",
794
- "newtab/index.html": "newtab",
795
- "sidepanel.html": "sidepanel",
796
- "sidepanel/index.html": "sidepanel",
797
- "*.sidepanel.html": "sidepanel",
798
- "*.sidepanel/index.html": "sidepanel",
799
- "devtools.html": "devtools",
800
- "devtools/index.html": "devtools",
801
- "background.[jt]s": "background",
802
- "background/index.[jt]s": "background",
803
- [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: "background",
804
- "content.[jt]s?(x)": "content-script",
805
- "content/index.[jt]s?(x)": "content-script",
806
- "*.content.[jt]s?(x)": "content-script",
807
- "*.content/index.[jt]s?(x)": "content-script",
808
- [`content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
809
- [`*.content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
810
- [`content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
811
- [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
812
- "popup.html": "popup",
813
- "popup/index.html": "popup",
814
- "options.html": "options",
815
- "options/index.html": "options",
816
- "*.html": "unlisted-page",
817
- "*/index.html": "unlisted-page",
818
- "*.[jt]s?(x)": "unlisted-script",
819
- "*/index.[jt]s?(x)": "unlisted-script",
820
- [`*.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style",
821
- [`*/index.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style"
822
- };
823
- var CONTENT_SCRIPT_OUT_DIR = "content-scripts";
824
560
 
825
- // src/core/utils/building/generate-wxt-dir.ts
561
+ // src/core/builders/vite/plugins/unimport.ts
826
562
  import { createUnimport } from "unimport";
827
- import fs4 from "fs-extra";
828
- import { relative as relative3, resolve as resolve4 } from "path";
829
-
830
- // src/core/utils/globals.ts
831
- function getGlobals(config) {
832
- return [
833
- {
834
- name: "MANIFEST_VERSION",
835
- value: config.manifestVersion,
563
+ import { extname as extname2 } from "path";
564
+ var ENABLED_EXTENSIONS = /* @__PURE__ */ new Set([
565
+ ".js",
566
+ ".jsx",
567
+ ".ts",
568
+ ".tsx",
569
+ ".vue",
570
+ ".svelte"
571
+ ]);
572
+ function unimport(config) {
573
+ const options = config.imports;
574
+ if (options === false)
575
+ return [];
576
+ const unimport2 = createUnimport(options);
577
+ return {
578
+ name: "wxt:unimport",
579
+ async config() {
580
+ await unimport2.scanImportsFromDir(void 0, { cwd: config.srcDir });
581
+ },
582
+ async transform(code, id) {
583
+ if (id.includes("node_modules"))
584
+ return;
585
+ if (!ENABLED_EXTENSIONS.has(extname2(id)))
586
+ return;
587
+ const injected = await unimport2.injectImports(code, id);
588
+ return {
589
+ code: injected.code,
590
+ map: injected.s.generateMap({ hires: "boundary", source: id })
591
+ };
592
+ }
593
+ };
594
+ }
595
+
596
+ // src/core/builders/vite/plugins/virtualEntrypoint.ts
597
+ import fs2 from "fs-extra";
598
+ import { resolve as resolve4 } from "path";
599
+ function virtualEntrypoint(type, config) {
600
+ const virtualId = `virtual:wxt-${type}?`;
601
+ const resolvedVirtualId = `\0${virtualId}`;
602
+ return {
603
+ name: `wxt:virtual-entrypoint`,
604
+ resolveId(id) {
605
+ const index = id.indexOf(virtualId);
606
+ if (index === -1)
607
+ return;
608
+ const inputPath = normalizePath(id.substring(index + virtualId.length));
609
+ return resolvedVirtualId + inputPath;
610
+ },
611
+ async load(id) {
612
+ if (!id.startsWith(resolvedVirtualId))
613
+ return;
614
+ const inputPath = id.replace(resolvedVirtualId, "");
615
+ const template = await fs2.readFile(
616
+ resolve4(config.wxtModuleDir, `dist/virtual/${type}-entrypoint.js`),
617
+ "utf-8"
618
+ );
619
+ return template.replace(`virtual:user-${type}`, inputPath);
620
+ }
621
+ };
622
+ }
623
+
624
+ // src/core/builders/vite/plugins/tsconfigPaths.ts
625
+ function tsconfigPaths(config) {
626
+ return {
627
+ name: "wxt:aliases",
628
+ async config() {
629
+ return {
630
+ resolve: {
631
+ alias: config.alias
632
+ }
633
+ };
634
+ }
635
+ };
636
+ }
637
+
638
+ // src/core/utils/constants.ts
639
+ var VIRTUAL_NOOP_BACKGROUND_MODULE_ID = "virtual:user-background";
640
+
641
+ // src/core/builders/vite/plugins/noopBackground.ts
642
+ function noopBackground() {
643
+ const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
644
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
645
+ return {
646
+ name: "wxt:noop-background",
647
+ resolveId(id) {
648
+ if (id === virtualModuleId)
649
+ return resolvedVirtualModuleId;
650
+ },
651
+ load(id) {
652
+ if (id === resolvedVirtualModuleId) {
653
+ return `import { defineBackground } from 'wxt/sandbox';
654
+ export default defineBackground(() => void 0)`;
655
+ }
656
+ }
657
+ };
658
+ }
659
+
660
+ // src/core/builders/vite/plugins/cssEntrypoints.ts
661
+ function cssEntrypoints(entrypoint, config) {
662
+ return {
663
+ name: "wxt:css-entrypoint",
664
+ config() {
665
+ return {
666
+ build: {
667
+ rollupOptions: {
668
+ output: {
669
+ assetFileNames: () => getEntrypointBundlePath(entrypoint, config.outDir, ".css")
670
+ }
671
+ }
672
+ }
673
+ };
674
+ },
675
+ generateBundle(_, bundle) {
676
+ Object.keys(bundle).forEach((file) => {
677
+ if (file.endsWith(".js"))
678
+ delete bundle[file];
679
+ });
680
+ }
681
+ };
682
+ }
683
+
684
+ // src/core/builders/vite/plugins/bundleAnalysis.ts
685
+ import { visualizer } from "@aklinker1/rollup-plugin-visualizer";
686
+ import path3 from "node:path";
687
+ var increment = 0;
688
+ function bundleAnalysis(config) {
689
+ return visualizer({
690
+ template: "raw-data",
691
+ filename: path3.resolve(
692
+ config.analysis.outputDir,
693
+ `${config.analysis.outputName}-${increment++}.json`
694
+ )
695
+ });
696
+ }
697
+
698
+ // src/core/utils/globals.ts
699
+ function getGlobals(config) {
700
+ return [
701
+ {
702
+ name: "MANIFEST_VERSION",
703
+ value: config.manifestVersion,
836
704
  type: `2 | 3`
837
705
  },
838
706
  {
@@ -882,985 +750,1108 @@ function getEntrypointGlobals(entrypointName) {
882
750
  ];
883
751
  }
884
752
 
885
- // src/core/utils/building/generate-wxt-dir.ts
886
- import path3 from "node:path";
887
-
888
- // src/core/utils/i18n.ts
889
- var predefinedMessages = {
890
- "@@extension_id": {
891
- message: "<browser.runtime.id>",
892
- description: "The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file."
893
- },
894
- "@@ui_locale": {
895
- message: "<browser.i18n.getUiLocale()>",
896
- description: ""
897
- },
898
- "@@bidi_dir": {
899
- message: "<ltr|rtl>",
900
- description: 'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.'
901
- },
902
- "@@bidi_reversed_dir": {
903
- message: "<rtl|ltr>",
904
- description: `If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".`
905
- },
906
- "@@bidi_start_edge": {
907
- message: "<left|right>",
908
- description: `If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".`
909
- },
910
- "@@bidi_end_edge": {
911
- message: "<right|left>",
912
- description: `If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".`
913
- }
914
- };
915
- function parseI18nMessages(messagesJson) {
916
- return Object.entries({
917
- ...predefinedMessages,
918
- ...messagesJson
919
- }).map(([name, details]) => ({
920
- name,
921
- ...details
922
- }));
753
+ // src/core/builders/vite/plugins/globals.ts
754
+ function globals(config) {
755
+ return {
756
+ name: "wxt:globals",
757
+ config() {
758
+ const define = {};
759
+ for (const global of getGlobals(config)) {
760
+ define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
761
+ }
762
+ return {
763
+ define
764
+ };
765
+ }
766
+ };
923
767
  }
924
768
 
925
- // src/core/utils/building/generate-wxt-dir.ts
926
- async function generateTypesDir(entrypoints) {
927
- await fs4.ensureDir(wxt.config.typesDir);
928
- const references = [];
929
- if (wxt.config.imports !== false) {
930
- const unimport2 = createUnimport(wxt.config.imports);
931
- references.push(await writeImportsDeclarationFile(unimport2));
932
- if (wxt.config.imports.eslintrc.enabled) {
933
- await writeImportsEslintFile(unimport2, wxt.config.imports);
769
+ // src/core/builders/vite/plugins/excludeBrowserPolyfill.ts
770
+ function excludeBrowserPolyfill(config) {
771
+ const virtualId = "virtual:wxt-webextension-polyfill-disabled";
772
+ return {
773
+ name: "wxt:exclude-browser-polyfill",
774
+ config() {
775
+ if (config.experimental.includeBrowserPolyfill)
776
+ return;
777
+ return {
778
+ resolve: {
779
+ alias: {
780
+ "webextension-polyfill": virtualId
781
+ }
782
+ }
783
+ };
784
+ },
785
+ load(id) {
786
+ if (id === virtualId) {
787
+ return "export default chrome";
788
+ }
934
789
  }
935
- }
936
- references.push(await writePathsDeclarationFile(entrypoints));
937
- references.push(await writeI18nDeclarationFile());
938
- references.push(await writeGlobalsDeclarationFile());
939
- const mainReference = await writeMainDeclarationFile(references);
940
- await writeTsConfigFile(mainReference);
941
- }
942
- async function writeImportsDeclarationFile(unimport2) {
943
- const filePath = resolve4(wxt.config.typesDir, "imports.d.ts");
944
- await unimport2.scanImportsFromDir(void 0, { cwd: wxt.config.srcDir });
945
- await writeFileIfDifferent(
946
- filePath,
947
- ["// Generated by wxt", await unimport2.generateTypeDeclarations()].join(
948
- "\n"
949
- ) + "\n"
950
- );
951
- return filePath;
952
- }
953
- async function writeImportsEslintFile(unimport2, options) {
954
- const globals2 = {};
955
- const eslintrc = { globals: globals2 };
956
- (await unimport2.getImports()).map((i) => i.as ?? i.name).filter(Boolean).sort().forEach((name) => {
957
- eslintrc.globals[name] = options.eslintrc.globalsPropValue;
958
- });
959
- await fs4.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
790
+ };
960
791
  }
961
- async function writePathsDeclarationFile(entrypoints) {
962
- const filePath = resolve4(wxt.config.typesDir, "paths.d.ts");
963
- const unions = entrypoints.map(
964
- (entry) => getEntrypointBundlePath(
965
- entry,
966
- wxt.config.outDir,
967
- isHtmlEntrypoint(entry) ? ".html" : ".js"
968
- )
969
- ).concat(await getPublicFiles()).map(normalizePath).map((path10) => ` | "/${path10}"`).sort().join("\n");
970
- const template = `// Generated by wxt
971
- import "wxt/browser";
972
792
 
973
- declare module "wxt/browser" {
974
- export type PublicPath =
975
- {{ union }}
976
- type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
977
- export interface WxtRuntime extends Runtime.Static {
978
- getURL(path: PublicPath): string;
979
- getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
980
- }
981
- }
982
- `;
983
- await writeFileIfDifferent(
984
- filePath,
985
- template.replace("{{ union }}", unions || " | never")
986
- );
987
- return filePath;
988
- }
989
- async function writeI18nDeclarationFile() {
990
- const filePath = resolve4(wxt.config.typesDir, "i18n.d.ts");
991
- const defaultLocale = wxt.config.manifest.default_locale;
992
- const template = `// Generated by wxt
993
- import "wxt/browser";
994
-
995
- declare module "wxt/browser" {
996
- /**
997
- * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
998
- */
999
- interface GetMessageOptions {
1000
- /**
1001
- * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
1002
- */
1003
- escapeLt?: boolean
1004
- }
1005
-
1006
- export interface WxtI18n extends I18n.Static {
1007
- {{ overrides }}
1008
- }
1009
- }
1010
- `;
1011
- let messages;
1012
- if (defaultLocale) {
1013
- const defaultLocalePath = path3.resolve(
1014
- wxt.config.publicDir,
1015
- "_locales",
1016
- defaultLocale,
1017
- "messages.json"
1018
- );
1019
- const content = JSON.parse(await fs4.readFile(defaultLocalePath, "utf-8"));
1020
- messages = parseI18nMessages(content);
1021
- } else {
1022
- messages = parseI18nMessages({});
1023
- }
1024
- const overrides = messages.map((message) => {
1025
- return ` /**
1026
- * ${message.description || "No message description."}
1027
- *
1028
- * "${message.message}"
1029
- */
1030
- getMessage(
1031
- messageName: "${message.name}",
1032
- substitutions?: string | string[],
1033
- options?: GetMessageOptions,
1034
- ): string;`;
1035
- });
1036
- await writeFileIfDifferent(
1037
- filePath,
1038
- template.replace("{{ overrides }}", overrides.join("\n"))
1039
- );
1040
- return filePath;
1041
- }
1042
- async function writeGlobalsDeclarationFile() {
1043
- const filePath = resolve4(wxt.config.typesDir, "globals.d.ts");
1044
- const globals2 = [...getGlobals(wxt.config), ...getEntrypointGlobals("")];
1045
- await writeFileIfDifferent(
1046
- filePath,
1047
- [
1048
- "// Generated by wxt",
1049
- "export {}",
1050
- "interface ImportMetaEnv {",
1051
- ...globals2.map((global) => ` readonly ${global.name}: ${global.type};`),
1052
- "}",
1053
- "interface ImportMeta {",
1054
- " readonly env: ImportMetaEnv",
1055
- "}"
1056
- ].join("\n") + "\n"
1057
- );
1058
- return filePath;
1059
- }
1060
- async function writeMainDeclarationFile(references) {
1061
- const dir = wxt.config.wxtDir;
1062
- const filePath = resolve4(dir, "wxt.d.ts");
1063
- await writeFileIfDifferent(
1064
- filePath,
1065
- [
1066
- "// Generated by wxt",
1067
- `/// <reference types="wxt/vite-builder-env" />`,
1068
- ...references.map(
1069
- (ref) => `/// <reference types="./${normalizePath(relative3(dir, ref))}" />`
1070
- )
1071
- ].join("\n") + "\n"
1072
- );
1073
- return filePath;
1074
- }
1075
- async function writeTsConfigFile(mainReference) {
1076
- const dir = wxt.config.wxtDir;
1077
- const getTsconfigPath = (path10) => normalizePath(relative3(dir, path10));
1078
- const paths = Object.entries(wxt.config.alias).flatMap(([alias, absolutePath]) => {
1079
- const aliasPath = getTsconfigPath(absolutePath);
1080
- return [
1081
- ` "${alias}": ["${aliasPath}"]`,
1082
- ` "${alias}/*": ["${aliasPath}/*"]`
1083
- ];
1084
- }).join(",\n");
1085
- await writeFileIfDifferent(
1086
- resolve4(dir, "tsconfig.json"),
1087
- `{
1088
- "compilerOptions": {
1089
- "target": "ESNext",
1090
- "module": "ESNext",
1091
- "moduleResolution": "Bundler",
1092
- "noEmit": true,
1093
- "esModuleInterop": true,
1094
- "forceConsistentCasingInFileNames": true,
1095
- "resolveJsonModule": true,
1096
- "strict": true,
1097
- "skipLibCheck": true,
1098
- "paths": {
1099
- ${paths}
793
+ // src/core/builders/vite/plugins/entrypointGroupGlobals.ts
794
+ function entrypointGroupGlobals(entrypointGroup) {
795
+ return {
796
+ name: "wxt:entrypoint-group-globals",
797
+ config() {
798
+ const define = {};
799
+ let name = Array.isArray(entrypointGroup) ? "html" : entrypointGroup.name;
800
+ for (const global of getEntrypointGlobals(name)) {
801
+ define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
802
+ }
803
+ return {
804
+ define
805
+ };
1100
806
  }
1101
- },
1102
- "include": [
1103
- "${getTsconfigPath(wxt.config.root)}/**/*",
1104
- "./${getTsconfigPath(mainReference)}"
1105
- ],
1106
- "exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
1107
- }`
1108
- );
807
+ };
1109
808
  }
1110
809
 
1111
- // src/core/utils/building/resolve-config.ts
1112
- import { loadConfig } from "c12";
1113
- import path5 from "node:path";
1114
-
1115
- // src/core/utils/cache.ts
1116
- import fs5, { ensureDir as ensureDir2 } from "fs-extra";
1117
- import { dirname as dirname2, resolve as resolve5 } from "path";
1118
- function createFsCache(wxtDir) {
1119
- const getPath = (key) => resolve5(wxtDir, "cache", encodeURIComponent(key));
810
+ // src/core/builders/vite/plugins/defineImportMeta.ts
811
+ function defineImportMeta() {
1120
812
  return {
1121
- async set(key, value) {
1122
- const path10 = getPath(key);
1123
- await ensureDir2(dirname2(path10));
1124
- await writeFileIfDifferent(path10, value);
1125
- },
1126
- async get(key) {
1127
- const path10 = getPath(key);
1128
- try {
1129
- return await fs5.readFile(path10, "utf-8");
1130
- } catch {
1131
- return void 0;
1132
- }
813
+ name: "wxt:define",
814
+ config() {
815
+ return {
816
+ define: {
817
+ // This works for all extension contexts, including background service worker
818
+ "import.meta.url": "self.location.href"
819
+ }
820
+ };
1133
821
  }
1134
822
  };
1135
823
  }
1136
824
 
1137
- // src/core/utils/building/resolve-config.ts
1138
- import consola, { LogLevels } from "consola";
1139
-
1140
- // src/core/builders/vite/plugins/devHtmlPrerender.ts
1141
- import { parseHTML as parseHTML2 } from "linkedom";
1142
- import { dirname as dirname3, relative as relative4, resolve as resolve6 } from "node:path";
1143
- var reactRefreshPreamble = "";
1144
- function devHtmlPrerender(config) {
1145
- const htmlReloadId = "@wxt/reload-html";
1146
- const resolvedHtmlReloadId = resolve6(
1147
- config.wxtModuleDir,
1148
- "dist/virtual/reload-html.js"
1149
- );
1150
- const virtualReactRefreshId = "@wxt/virtual-react-refresh";
1151
- const resolvedVirtualReactRefreshId = "\0" + virtualReactRefreshId;
1152
- return [
1153
- {
1154
- apply: "build",
1155
- name: "wxt:dev-html-prerender",
1156
- config() {
1157
- return {
1158
- resolve: {
1159
- alias: {
1160
- [htmlReloadId]: resolvedHtmlReloadId
825
+ // src/core/builders/vite/index.ts
826
+ async function createViteBuilder(wxtConfig, server) {
827
+ const vite = await import("vite");
828
+ const getBaseConfig = async () => {
829
+ const config = await wxtConfig.vite(wxtConfig.env);
830
+ config.root = wxtConfig.root;
831
+ config.configFile = false;
832
+ config.logLevel = "warn";
833
+ config.mode = wxtConfig.mode;
834
+ config.build ??= {};
835
+ config.build.outDir = wxtConfig.outDir;
836
+ config.build.emptyOutDir = false;
837
+ if (config.build.minify == null && wxtConfig.command === "serve") {
838
+ config.build.minify = false;
839
+ }
840
+ if (config.build.sourcemap == null && wxtConfig.command === "serve") {
841
+ config.build.sourcemap = "inline";
842
+ }
843
+ config.plugins ??= [];
844
+ config.plugins.push(
845
+ download(wxtConfig),
846
+ devHtmlPrerender(wxtConfig, server),
847
+ unimport(wxtConfig),
848
+ virtualEntrypoint("background", wxtConfig),
849
+ virtualEntrypoint("content-script-isolated-world", wxtConfig),
850
+ virtualEntrypoint("content-script-main-world", wxtConfig),
851
+ virtualEntrypoint("unlisted-script", wxtConfig),
852
+ devServerGlobals(wxtConfig, server),
853
+ tsconfigPaths(wxtConfig),
854
+ noopBackground(),
855
+ globals(wxtConfig),
856
+ excludeBrowserPolyfill(wxtConfig),
857
+ defineImportMeta()
858
+ );
859
+ if (wxtConfig.analysis.enabled) {
860
+ config.plugins.push(bundleAnalysis(wxtConfig));
861
+ }
862
+ return config;
863
+ };
864
+ const getLibModeConfig = (entrypoint) => {
865
+ const entry = getRollupEntry(entrypoint);
866
+ const plugins = [
867
+ entrypointGroupGlobals(entrypoint)
868
+ ];
869
+ if (entrypoint.type === "content-script-style" || entrypoint.type === "unlisted-style") {
870
+ plugins.push(cssEntrypoints(entrypoint, wxtConfig));
871
+ }
872
+ const libMode = {
873
+ mode: wxtConfig.mode,
874
+ plugins,
875
+ build: {
876
+ lib: {
877
+ entry,
878
+ formats: ["iife"],
879
+ name: "_",
880
+ fileName: entrypoint.name
881
+ },
882
+ rollupOptions: {
883
+ output: {
884
+ // There's only a single output for this build, so we use the desired bundle path for the
885
+ // entry output (like "content-scripts/overlay.js")
886
+ entryFileNames: getEntrypointBundlePath(
887
+ entrypoint,
888
+ wxtConfig.outDir,
889
+ ".js"
890
+ ),
891
+ // Output content script CSS to `content-scripts/`, but all other scripts are written to
892
+ // `assets/`.
893
+ assetFileNames: ({ name }) => {
894
+ if (entrypoint.type === "content-script" && name?.endsWith("css")) {
895
+ return `content-scripts/${entrypoint.name}.[ext]`;
896
+ } else {
897
+ return `assets/${entrypoint.name}.[ext]`;
898
+ }
1161
899
  }
1162
900
  }
1163
- };
901
+ }
1164
902
  },
1165
- // Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
1166
- // before the paths are replaced with their bundled path
1167
- transform(code, id) {
1168
- const server = config.server;
1169
- if (config.command !== "serve" || server == null || !id.endsWith(".html"))
1170
- return;
1171
- const { document } = parseHTML2(code);
1172
- const _pointToDevServer = (querySelector, attr) => pointToDevServer(config, server, id, document, querySelector, attr);
1173
- _pointToDevServer("script[type=module]", "src");
1174
- _pointToDevServer("link[rel=stylesheet]", "href");
1175
- const reloader = document.createElement("script");
1176
- reloader.src = htmlReloadId;
1177
- reloader.type = "module";
1178
- document.head.appendChild(reloader);
1179
- const newHtml = document.toString();
1180
- config.logger.debug("transform " + id);
1181
- config.logger.debug("Old HTML:\n" + code);
1182
- config.logger.debug("New HTML:\n" + newHtml);
1183
- return newHtml;
1184
- },
1185
- // Pass the HTML through the dev server to add dev-mode specific code
1186
- async transformIndexHtml(html, ctx) {
1187
- const server = config.server;
1188
- if (config.command !== "serve" || server == null)
1189
- return;
1190
- const originalUrl = `${server.origin}${ctx.path}`;
1191
- const name = getEntrypointName(config.entrypointsDir, ctx.filename);
1192
- const url = `${server.origin}/${name}.html`;
1193
- const serverHtml = await server.transformHtml(url, html, originalUrl);
1194
- const { document } = parseHTML2(serverHtml);
1195
- const reactRefreshScript = Array.from(
1196
- document.querySelectorAll("script[type=module]")
1197
- ).find((script) => script.innerHTML.includes("@react-refresh"));
1198
- if (reactRefreshScript) {
1199
- reactRefreshPreamble = reactRefreshScript.innerHTML;
1200
- const virtualScript = document.createElement("script");
1201
- virtualScript.type = "module";
1202
- virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
1203
- reactRefreshScript.replaceWith(virtualScript);
1204
- }
1205
- const viteClientScript = document.querySelector(
1206
- "script[src='/@vite/client']"
1207
- );
1208
- if (viteClientScript) {
1209
- viteClientScript.src = `${server.origin}${viteClientScript.src}`;
1210
- }
1211
- const newHtml = document.toString();
1212
- config.logger.debug("transformIndexHtml " + ctx.filename);
1213
- config.logger.debug("Old HTML:\n" + html);
1214
- config.logger.debug("New HTML:\n" + newHtml);
1215
- return newHtml;
903
+ define: {
904
+ // See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
905
+ "process.env.NODE_ENV": JSON.stringify(wxtConfig.mode)
1216
906
  }
1217
- },
1218
- {
1219
- name: "wxt:virtualize-react-refresh",
1220
- apply: "serve",
1221
- resolveId(id) {
1222
- if (id === `/${virtualReactRefreshId}`) {
1223
- return resolvedVirtualReactRefreshId;
1224
- }
1225
- if (id.startsWith("/chunks/")) {
1226
- return "\0noop";
1227
- }
1228
- },
1229
- load(id) {
1230
- if (id === resolvedVirtualReactRefreshId) {
1231
- return reactRefreshPreamble;
1232
- }
1233
- if (id === "\0noop") {
1234
- return "";
907
+ };
908
+ return libMode;
909
+ };
910
+ const getMultiPageConfig = (entrypoints) => {
911
+ const htmlEntrypoints = new Set(
912
+ entrypoints.filter(isHtmlEntrypoint).map((e) => e.name)
913
+ );
914
+ return {
915
+ mode: wxtConfig.mode,
916
+ plugins: [
917
+ multipageMove(entrypoints, wxtConfig),
918
+ entrypointGroupGlobals(entrypoints)
919
+ ],
920
+ build: {
921
+ rollupOptions: {
922
+ input: entrypoints.reduce((input, entry) => {
923
+ input[entry.name] = getRollupEntry(entry);
924
+ return input;
925
+ }, {}),
926
+ output: {
927
+ // Include a hash to prevent conflicts
928
+ chunkFileNames: "chunks/[name]-[hash].js",
929
+ entryFileNames: ({ name }) => {
930
+ if (htmlEntrypoints.has(name))
931
+ return "chunks/[name]-[hash].js";
932
+ return "[name].js";
933
+ },
934
+ // We can't control the "name", so we need a hash to prevent conflicts
935
+ assetFileNames: "assets/[name]-[hash].[ext]"
936
+ }
1235
937
  }
1236
938
  }
1237
- }
1238
- ];
1239
- }
1240
- function pointToDevServer(config, server, id, document, querySelector, attr) {
1241
- document.querySelectorAll(querySelector).forEach((element) => {
1242
- const src = element.getAttribute(attr);
1243
- if (!src || isUrl(src))
1244
- return;
1245
- let resolvedAbsolutePath;
1246
- const matchingAlias = Object.entries(config.alias).find(
1247
- ([key]) => src.startsWith(key)
1248
- );
1249
- if (matchingAlias) {
1250
- const [alias, replacement] = matchingAlias;
1251
- resolvedAbsolutePath = resolve6(
1252
- config.root,
1253
- src.replace(alias, replacement)
1254
- );
1255
- } else {
1256
- resolvedAbsolutePath = resolve6(dirname3(id), src);
1257
- }
1258
- if (resolvedAbsolutePath) {
1259
- const relativePath = normalizePath(
1260
- relative4(config.root, resolvedAbsolutePath)
1261
- );
1262
- if (relativePath.startsWith(".")) {
1263
- let path10 = normalizePath(resolvedAbsolutePath);
1264
- if (!path10.startsWith("/"))
1265
- path10 = "/" + path10;
1266
- element.setAttribute(attr, `${server.origin}/@fs${path10}`);
1267
- } else {
1268
- const url = new URL(relativePath, server.origin);
1269
- element.setAttribute(attr, url.href);
939
+ };
940
+ };
941
+ const getCssConfig = (entrypoint) => {
942
+ return {
943
+ mode: wxtConfig.mode,
944
+ plugins: [entrypointGroupGlobals(entrypoint)],
945
+ build: {
946
+ rollupOptions: {
947
+ input: {
948
+ [entrypoint.name]: entrypoint.inputPath
949
+ },
950
+ output: {
951
+ assetFileNames: () => {
952
+ if (entrypoint.type === "content-script-style") {
953
+ return `content-scripts/${entrypoint.name}.[ext]`;
954
+ } else {
955
+ return `assets/${entrypoint.name}.[ext]`;
956
+ }
957
+ }
958
+ }
959
+ }
1270
960
  }
1271
- }
1272
- });
1273
- }
1274
- function isUrl(str) {
1275
- try {
1276
- new URL(str);
1277
- return true;
1278
- } catch {
1279
- return false;
1280
- }
1281
- }
1282
-
1283
- // src/core/builders/vite/plugins/devServerGlobals.ts
1284
- function devServerGlobals(config) {
961
+ };
962
+ };
1285
963
  return {
1286
- name: "wxt:dev-server-globals",
1287
- config() {
1288
- if (config.server == null || config.command == "build")
1289
- return;
964
+ name: "Vite",
965
+ version: vite.version,
966
+ async build(group) {
967
+ let entryConfig;
968
+ if (Array.isArray(group))
969
+ entryConfig = getMultiPageConfig(group);
970
+ else if (group.inputPath.endsWith(".css"))
971
+ entryConfig = getCssConfig(group);
972
+ else
973
+ entryConfig = getLibModeConfig(group);
974
+ const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
975
+ const result = await vite.build(buildConfig);
1290
976
  return {
1291
- define: {
1292
- __DEV_SERVER_PROTOCOL__: JSON.stringify("ws:"),
1293
- __DEV_SERVER_HOSTNAME__: JSON.stringify(config.server.hostname),
1294
- __DEV_SERVER_PORT__: JSON.stringify(config.server.port)
977
+ entrypoints: group,
978
+ chunks: getBuildOutputChunks(result)
979
+ };
980
+ },
981
+ async createServer(info) {
982
+ const serverConfig = {
983
+ server: {
984
+ port: info.port,
985
+ strictPort: true,
986
+ host: info.hostname,
987
+ origin: info.origin
1295
988
  }
1296
989
  };
990
+ const baseConfig = await getBaseConfig();
991
+ const viteServer = await vite.createServer(
992
+ vite.mergeConfig(baseConfig, serverConfig)
993
+ );
994
+ const server2 = {
995
+ async listen() {
996
+ await viteServer.listen(info.port);
997
+ },
998
+ async close() {
999
+ await viteServer.close();
1000
+ },
1001
+ transformHtml(...args) {
1002
+ return viteServer.transformIndexHtml(...args);
1003
+ },
1004
+ ws: {
1005
+ send(message, payload) {
1006
+ return viteServer.ws.send(message, payload);
1007
+ },
1008
+ on(message, cb) {
1009
+ viteServer.ws.on(message, cb);
1010
+ }
1011
+ },
1012
+ watcher: viteServer.watcher
1013
+ };
1014
+ return server2;
1297
1015
  }
1298
1016
  };
1299
1017
  }
1300
-
1301
- // src/core/utils/network.ts
1302
- import dns from "node:dns";
1303
-
1304
- // src/core/utils/time.ts
1305
- function formatDuration(duration) {
1306
- if (duration < 1e3)
1307
- return `${duration} ms`;
1308
- if (duration < 1e4)
1309
- return `${(duration / 1e3).toFixed(3)} s`;
1310
- if (duration < 6e4)
1311
- return `${(duration / 1e3).toFixed(1)} s`;
1312
- return `${(duration / 1e3).toFixed(0)} s`;
1018
+ function getBuildOutputChunks(result) {
1019
+ if ("on" in result)
1020
+ throw Error("wxt does not support vite watch mode.");
1021
+ if (Array.isArray(result))
1022
+ return result.flatMap(({ output }) => output);
1023
+ return result.output;
1313
1024
  }
1314
- function withTimeout(promise, duration) {
1315
- return new Promise((res, rej) => {
1316
- const timeout = setTimeout(() => {
1317
- rej(`Promise timed out after ${duration}ms`);
1318
- }, duration);
1319
- promise.then(res).catch(rej).finally(() => clearTimeout(timeout));
1320
- });
1025
+ function getRollupEntry(entrypoint) {
1026
+ let virtualEntrypointType;
1027
+ switch (entrypoint.type) {
1028
+ case "background":
1029
+ case "unlisted-script":
1030
+ virtualEntrypointType = entrypoint.type;
1031
+ break;
1032
+ case "content-script":
1033
+ virtualEntrypointType = entrypoint.options.world === "MAIN" ? "content-script-main-world" : "content-script-isolated-world";
1034
+ break;
1035
+ }
1036
+ return virtualEntrypointType ? `virtual:wxt-${virtualEntrypointType}?${entrypoint.inputPath}` : entrypoint.inputPath;
1321
1037
  }
1322
1038
 
1323
- // src/core/utils/network.ts
1324
- function isOffline() {
1325
- const isOffline2 = new Promise((res) => {
1326
- dns.resolve("google.com", (err) => {
1327
- if (err == null) {
1328
- res(false);
1329
- } else {
1330
- res(true);
1331
- }
1332
- });
1333
- });
1334
- return withTimeout(isOffline2, 1e3).catch(() => true);
1335
- }
1336
- async function isOnline() {
1337
- const offline = await isOffline();
1338
- return !offline;
1339
- }
1340
- async function fetchCached(url, config) {
1341
- let content = "";
1342
- if (await isOnline()) {
1343
- const res = await fetch(url);
1344
- if (res.status < 300) {
1345
- content = await res.text();
1346
- await config.fsCache.set(url, content);
1347
- } else {
1348
- config.logger.debug(
1349
- `Failed to download "${url}", falling back to cache...`
1350
- );
1351
- }
1352
- }
1353
- if (!content)
1354
- content = await config.fsCache.get(url) ?? "";
1355
- if (!content)
1356
- throw Error(
1357
- `Offline and "${url}" has not been cached. Try again when online.`
1358
- );
1359
- return content;
1360
- }
1361
-
1362
- // src/core/builders/vite/plugins/download.ts
1363
- function download(config) {
1364
- return {
1365
- name: "wxt:download",
1366
- resolveId(id) {
1367
- if (id.startsWith("url:"))
1368
- return "\0" + id;
1039
+ // src/core/wxt.ts
1040
+ var wxt;
1041
+ async function registerWxt(command, inlineConfig = {}, getServer) {
1042
+ const hooks = createHooks();
1043
+ const config = await resolveConfig(inlineConfig, command);
1044
+ const server = await getServer?.(config);
1045
+ const builder = await createViteBuilder(config, server);
1046
+ const pm = await createWxtPackageManager(config.root);
1047
+ wxt = {
1048
+ config,
1049
+ hooks,
1050
+ get logger() {
1051
+ return config.logger;
1369
1052
  },
1370
- async load(id) {
1371
- if (!id.startsWith("\0url:"))
1372
- return;
1373
- const url = id.replace("\0url:", "");
1374
- return await fetchCached(url, config);
1375
- }
1053
+ async reloadConfig() {
1054
+ wxt.config = await resolveConfig(inlineConfig, command);
1055
+ },
1056
+ pm,
1057
+ builder,
1058
+ server
1376
1059
  };
1060
+ wxt.hooks.addHooks(config.hooks);
1061
+ await wxt.hooks.callHook("ready", wxt);
1377
1062
  }
1378
1063
 
1379
- // src/core/builders/vite/plugins/multipageMove.ts
1380
- import { dirname as dirname4, extname, resolve as resolve7, join } from "node:path";
1381
- import fs6, { ensureDir as ensureDir3 } from "fs-extra";
1382
- function multipageMove(entrypoints, config) {
1383
- return {
1384
- name: "wxt:multipage-move",
1385
- async writeBundle(_, bundle) {
1386
- for (const oldBundlePath in bundle) {
1387
- const entrypoint = entrypoints.find(
1388
- (entry) => !!normalizePath(entry.inputPath).endsWith(oldBundlePath)
1389
- );
1390
- if (entrypoint == null) {
1391
- config.logger.debug(
1392
- `No entrypoint found for ${oldBundlePath}, leaving in chunks directory`
1393
- );
1394
- continue;
1395
- }
1396
- const newBundlePath = getEntrypointBundlePath(
1397
- entrypoint,
1398
- config.outDir,
1399
- extname(oldBundlePath)
1400
- );
1401
- if (newBundlePath === oldBundlePath) {
1402
- config.logger.debug(
1403
- "HTML file is already in the correct location",
1404
- oldBundlePath
1405
- );
1406
- continue;
1407
- }
1408
- const oldAbsPath = resolve7(config.outDir, oldBundlePath);
1409
- const newAbsPath = resolve7(config.outDir, newBundlePath);
1410
- await ensureDir3(dirname4(newAbsPath));
1411
- await fs6.move(oldAbsPath, newAbsPath, { overwrite: true });
1412
- const renamedChunk = {
1413
- ...bundle[oldBundlePath],
1414
- fileName: newBundlePath
1415
- };
1416
- delete bundle[oldBundlePath];
1417
- bundle[newBundlePath] = renamedChunk;
1418
- }
1419
- removeEmptyDirs(config.outDir);
1420
- }
1421
- };
1422
- }
1423
- async function removeEmptyDirs(dir) {
1424
- const files = await fs6.readdir(dir);
1425
- for (const file of files) {
1426
- const filePath = join(dir, file);
1427
- const stats = await fs6.stat(filePath);
1428
- if (stats.isDirectory()) {
1429
- await removeEmptyDirs(filePath);
1430
- }
1431
- }
1432
- try {
1433
- await fs6.rmdir(dir);
1434
- } catch {
1064
+ // src/core/utils/fs.ts
1065
+ async function writeFileIfDifferent(file, newContents) {
1066
+ const existingContents = await fs3.readFile(file, "utf-8").catch(() => void 0);
1067
+ if (existingContents !== newContents) {
1068
+ await fs3.writeFile(file, newContents);
1435
1069
  }
1436
1070
  }
1437
-
1438
- // src/core/builders/vite/plugins/unimport.ts
1439
- import { createUnimport as createUnimport2 } from "unimport";
1440
- import { extname as extname2 } from "path";
1441
- var ENABLED_EXTENSIONS = /* @__PURE__ */ new Set([
1442
- ".js",
1443
- ".jsx",
1444
- ".ts",
1445
- ".tsx",
1446
- ".vue",
1447
- ".svelte"
1448
- ]);
1449
- function unimport(config) {
1450
- const options = config.imports;
1451
- if (options === false)
1071
+ async function getPublicFiles() {
1072
+ if (!await fs3.exists(wxt.config.publicDir))
1452
1073
  return [];
1453
- const unimport2 = createUnimport2(options);
1454
- return {
1455
- name: "wxt:unimport",
1456
- async config() {
1457
- await unimport2.scanImportsFromDir(void 0, { cwd: config.srcDir });
1458
- },
1459
- async transform(code, id) {
1460
- if (id.includes("node_modules"))
1461
- return;
1462
- if (!ENABLED_EXTENSIONS.has(extname2(id)))
1463
- return;
1464
- const injected = await unimport2.injectImports(code, id);
1465
- return {
1466
- code: injected.code,
1467
- map: injected.s.generateMap({ hires: "boundary", source: id })
1468
- };
1469
- }
1470
- };
1074
+ const files = await glob("**/*", { cwd: wxt.config.publicDir });
1075
+ return files.map(unnormalizePath);
1471
1076
  }
1472
1077
 
1473
- // src/core/builders/vite/plugins/virtualEntrypoint.ts
1474
- import fs7 from "fs-extra";
1475
- import { resolve as resolve8 } from "path";
1476
- function virtualEntrypoint(type, config) {
1477
- const virtualId = `virtual:wxt-${type}?`;
1478
- const resolvedVirtualId = `\0${virtualId}`;
1479
- return {
1480
- name: `wxt:virtual-entrypoint`,
1481
- resolveId(id) {
1482
- const index = id.indexOf(virtualId);
1483
- if (index === -1)
1484
- return;
1485
- const inputPath = normalizePath(id.substring(index + virtualId.length));
1486
- return resolvedVirtualId + inputPath;
1487
- },
1488
- async load(id) {
1489
- if (!id.startsWith(resolvedVirtualId))
1490
- return;
1491
- const inputPath = id.replace(resolvedVirtualId, "");
1492
- const template = await fs7.readFile(
1493
- resolve8(config.wxtModuleDir, `dist/virtual/${type}-entrypoint.js`),
1494
- "utf-8"
1495
- );
1496
- return template.replace(`virtual:user-${type}`, inputPath);
1078
+ // src/core/utils/building/build-entrypoints.ts
1079
+ import fs4 from "fs-extra";
1080
+ import { dirname as dirname3, resolve as resolve5 } from "path";
1081
+ import pc from "picocolors";
1082
+ async function buildEntrypoints(groups, spinner) {
1083
+ const steps = [];
1084
+ for (let i = 0; i < groups.length; i++) {
1085
+ const group = groups[i];
1086
+ const groupNames = [group].flat().map((e) => e.name);
1087
+ const groupNameColored = groupNames.join(pc.dim(", "));
1088
+ spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
1089
+ try {
1090
+ steps.push(await wxt.builder.build(group));
1091
+ } catch (err) {
1092
+ spinner.stop().clear();
1093
+ wxt.logger.error(err);
1094
+ throw Error(`Failed to build ${groupNames.join(", ")}`, { cause: err });
1497
1095
  }
1498
- };
1096
+ }
1097
+ const publicAssets = await copyPublicDirectory();
1098
+ return { publicAssets, steps };
1499
1099
  }
1500
-
1501
- // src/core/builders/vite/plugins/tsconfigPaths.ts
1502
- function tsconfigPaths(config) {
1503
- return {
1504
- name: "wxt:aliases",
1505
- async config() {
1506
- return {
1507
- resolve: {
1508
- alias: config.alias
1509
- }
1510
- };
1511
- }
1512
- };
1100
+ async function copyPublicDirectory() {
1101
+ const files = await getPublicFiles();
1102
+ if (files.length === 0)
1103
+ return [];
1104
+ const publicAssets = [];
1105
+ for (const file of files) {
1106
+ const srcPath = resolve5(wxt.config.publicDir, file);
1107
+ const outPath = resolve5(wxt.config.outDir, file);
1108
+ await fs4.ensureDir(dirname3(outPath));
1109
+ await fs4.copyFile(srcPath, outPath);
1110
+ publicAssets.push({
1111
+ type: "asset",
1112
+ fileName: file
1113
+ });
1114
+ }
1115
+ return publicAssets;
1513
1116
  }
1514
1117
 
1515
- // src/core/builders/vite/plugins/noopBackground.ts
1516
- function noopBackground() {
1517
- const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
1518
- const resolvedVirtualModuleId = "\0" + virtualModuleId;
1519
- return {
1520
- name: "wxt:noop-background",
1521
- resolveId(id) {
1522
- if (id === virtualModuleId)
1523
- return resolvedVirtualModuleId;
1524
- },
1525
- load(id) {
1526
- if (id === resolvedVirtualModuleId) {
1527
- return `import { defineBackground } from 'wxt/sandbox';
1528
- export default defineBackground(() => void 0)`;
1529
- }
1530
- }
1531
- };
1118
+ // src/core/utils/arrays.ts
1119
+ function every(array, predicate) {
1120
+ for (let i = 0; i < array.length; i++)
1121
+ if (!predicate(array[i], i))
1122
+ return false;
1123
+ return true;
1124
+ }
1125
+ function some(array, predicate) {
1126
+ for (let i = 0; i < array.length; i++)
1127
+ if (predicate(array[i], i))
1128
+ return true;
1129
+ return false;
1532
1130
  }
1533
1131
 
1534
- // src/core/builders/vite/plugins/cssEntrypoints.ts
1535
- function cssEntrypoints(entrypoint, config) {
1536
- return {
1537
- name: "wxt:css-entrypoint",
1538
- config() {
1539
- return {
1540
- build: {
1541
- rollupOptions: {
1542
- output: {
1543
- assetFileNames: () => getEntrypointBundlePath(entrypoint, config.outDir, ".css")
1544
- }
1545
- }
1546
- }
1547
- };
1548
- },
1549
- generateBundle(_, bundle) {
1550
- Object.keys(bundle).forEach((file) => {
1551
- if (file.endsWith(".js"))
1552
- delete bundle[file];
1553
- });
1554
- }
1555
- };
1556
- }
1557
-
1558
- // src/core/builders/vite/plugins/bundleAnalysis.ts
1559
- import { visualizer } from "@aklinker1/rollup-plugin-visualizer";
1560
- import path4 from "node:path";
1561
- var increment = 0;
1562
- function bundleAnalysis(config) {
1563
- return visualizer({
1564
- template: "raw-data",
1565
- filename: path4.resolve(
1566
- config.analysis.outputDir,
1567
- `${config.analysis.outputName}-${increment++}.json`
1132
+ // src/core/utils/building/detect-dev-changes.ts
1133
+ function detectDevChanges(changedFiles, currentOutput) {
1134
+ const isConfigChange = some(
1135
+ changedFiles,
1136
+ (file) => file === wxt.config.userConfigMetadata.configFile
1137
+ );
1138
+ if (isConfigChange)
1139
+ return { type: "full-restart" };
1140
+ const isRunnerChange = some(
1141
+ changedFiles,
1142
+ (file) => file === wxt.config.runnerConfig.configFile
1143
+ );
1144
+ if (isRunnerChange)
1145
+ return { type: "browser-restart" };
1146
+ const changedSteps = new Set(
1147
+ changedFiles.flatMap(
1148
+ (changedFile) => findEffectedSteps(changedFile, currentOutput)
1568
1149
  )
1569
- });
1570
- }
1571
-
1572
- // src/core/builders/vite/plugins/globals.ts
1573
- function globals(config) {
1574
- return {
1575
- name: "wxt:globals",
1576
- config() {
1577
- const define = {};
1578
- for (const global of getGlobals(config)) {
1579
- define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
1580
- }
1581
- return {
1582
- define
1583
- };
1584
- }
1150
+ );
1151
+ if (changedSteps.size === 0)
1152
+ return { type: "no-change" };
1153
+ const unchangedOutput = {
1154
+ manifest: currentOutput.manifest,
1155
+ steps: [],
1156
+ publicAssets: []
1585
1157
  };
1586
- }
1587
-
1588
- // src/core/builders/vite/plugins/excludeBrowserPolyfill.ts
1589
- function excludeBrowserPolyfill(config) {
1590
- const virtualId = "virtual:wxt-webextension-polyfill-disabled";
1591
- return {
1592
- name: "wxt:exclude-browser-polyfill",
1593
- config() {
1594
- if (config.experimental.includeBrowserPolyfill)
1595
- return;
1596
- return {
1597
- resolve: {
1598
- alias: {
1599
- "webextension-polyfill": virtualId
1600
- }
1601
- }
1602
- };
1603
- },
1604
- load(id) {
1605
- if (id === virtualId) {
1606
- return "export default chrome";
1607
- }
1608
- }
1158
+ const changedOutput = {
1159
+ manifest: currentOutput.manifest,
1160
+ steps: [],
1161
+ publicAssets: []
1609
1162
  };
1610
- }
1611
-
1612
- // src/core/builders/vite/plugins/entrypointGroupGlobals.ts
1613
- function entrypointGroupGlobals(entrypointGroup) {
1614
- return {
1615
- name: "wxt:entrypoint-group-globals",
1616
- config() {
1617
- const define = {};
1618
- let name = Array.isArray(entrypointGroup) ? "html" : entrypointGroup.name;
1619
- for (const global of getEntrypointGlobals(name)) {
1620
- define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
1621
- }
1622
- return {
1623
- define
1624
- };
1163
+ for (const step of currentOutput.steps) {
1164
+ if (changedSteps.has(step)) {
1165
+ changedOutput.steps.push(step);
1166
+ } else {
1167
+ unchangedOutput.steps.push(step);
1625
1168
  }
1626
- };
1627
- }
1628
-
1629
- // src/core/builders/vite/plugins/defineImportMeta.ts
1630
- function defineImportMeta() {
1631
- return {
1632
- name: "wxt:define",
1633
- config() {
1634
- return {
1635
- define: {
1636
- // This works for all extension contexts, including background service worker
1637
- "import.meta.url": "self.location.href"
1638
- }
1639
- };
1169
+ }
1170
+ for (const asset of currentOutput.publicAssets) {
1171
+ if (changedSteps.has(asset)) {
1172
+ changedOutput.publicAssets.push(asset);
1173
+ } else {
1174
+ unchangedOutput.publicAssets.push(asset);
1640
1175
  }
1176
+ }
1177
+ const isOnlyHtmlChanges = changedFiles.length > 0 && every(changedFiles, (file) => file.endsWith(".html"));
1178
+ if (isOnlyHtmlChanges) {
1179
+ return {
1180
+ type: "html-reload",
1181
+ cachedOutput: unchangedOutput,
1182
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
1183
+ };
1184
+ }
1185
+ const isOnlyContentScripts = changedOutput.steps.length > 0 && every(
1186
+ changedOutput.steps.flatMap((step) => step.entrypoints),
1187
+ (entry) => entry.type === "content-script"
1188
+ );
1189
+ if (isOnlyContentScripts) {
1190
+ return {
1191
+ type: "content-script-reload",
1192
+ cachedOutput: unchangedOutput,
1193
+ changedSteps: changedOutput.steps,
1194
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
1195
+ };
1196
+ }
1197
+ return {
1198
+ type: "extension-reload",
1199
+ cachedOutput: unchangedOutput,
1200
+ rebuildGroups: changedOutput.steps.map((step) => step.entrypoints)
1641
1201
  };
1642
1202
  }
1203
+ function findEffectedSteps(changedFile, currentOutput) {
1204
+ const changes = [];
1205
+ const changedPath = normalizePath(changedFile);
1206
+ const isChunkEffected = (chunk) => (
1207
+ // If it's an HTML file with the same path, is is effected because HTML files need to be re-rendered
1208
+ // - fileName is normalized, relative bundle path, "<entrypoint-name>.html"
1209
+ chunk.type === "asset" && changedPath.replace("/index.html", ".html").endsWith(chunk.fileName) || // If it's a chunk that depends on the changed file, it is effected
1210
+ // - moduleIds are absolute, normalized paths
1211
+ chunk.type === "chunk" && chunk.moduleIds.includes(changedPath)
1212
+ );
1213
+ for (const step of currentOutput.steps) {
1214
+ const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
1215
+ if (effectedChunk)
1216
+ changes.push(step);
1217
+ }
1218
+ const effectedAsset = currentOutput.publicAssets.find(
1219
+ (chunk) => isChunkEffected(chunk)
1220
+ );
1221
+ if (effectedAsset)
1222
+ changes.push(effectedAsset);
1223
+ return changes;
1224
+ }
1643
1225
 
1644
- // src/core/builders/vite/index.ts
1645
- async function createViteBuilder(inlineConfig, userConfig, wxtConfig) {
1646
- const vite = await import("vite");
1647
- const getBaseConfig = async () => {
1648
- const resolvedInlineConfig = await inlineConfig.vite?.(wxtConfig.env) ?? {};
1649
- const resolvedUserConfig = await userConfig.vite?.(wxtConfig.env) ?? {};
1650
- const config = vite.mergeConfig(
1651
- resolvedUserConfig,
1652
- resolvedInlineConfig
1653
- );
1654
- config.root = wxtConfig.root;
1655
- config.configFile = false;
1656
- config.logLevel = "warn";
1657
- config.mode = wxtConfig.mode;
1658
- config.build ??= {};
1659
- config.build.outDir = wxtConfig.outDir;
1660
- config.build.emptyOutDir = false;
1661
- if (config.build.minify == null && wxtConfig.command === "serve") {
1662
- config.build.minify = false;
1663
- }
1664
- if (config.build.sourcemap == null && wxtConfig.command === "serve") {
1665
- config.build.sourcemap = "inline";
1666
- }
1667
- config.plugins ??= [];
1668
- config.plugins.push(
1669
- download(wxtConfig),
1670
- devHtmlPrerender(wxtConfig),
1671
- unimport(wxtConfig),
1672
- virtualEntrypoint("background", wxtConfig),
1673
- virtualEntrypoint("content-script-isolated-world", wxtConfig),
1674
- virtualEntrypoint("content-script-main-world", wxtConfig),
1675
- virtualEntrypoint("unlisted-script", wxtConfig),
1676
- devServerGlobals(wxtConfig),
1677
- tsconfigPaths(wxtConfig),
1678
- noopBackground(),
1679
- globals(wxtConfig),
1680
- excludeBrowserPolyfill(wxtConfig),
1681
- defineImportMeta()
1226
+ // src/core/utils/building/find-entrypoints.ts
1227
+ import { relative as relative3, resolve as resolve6 } from "path";
1228
+ import fs5 from "fs-extra";
1229
+ import { minimatch } from "minimatch";
1230
+ import { parseHTML as parseHTML2 } from "linkedom";
1231
+ import JSON5 from "json5";
1232
+ import glob2 from "fast-glob";
1233
+ import pc2 from "picocolors";
1234
+ async function findEntrypoints() {
1235
+ const relativePaths = await glob2(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
1236
+ cwd: wxt.config.entrypointsDir
1237
+ });
1238
+ relativePaths.sort();
1239
+ const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
1240
+ const entrypointInfos = relativePaths.reduce((results, relativePath) => {
1241
+ const inputPath = resolve6(wxt.config.entrypointsDir, relativePath);
1242
+ const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
1243
+ const matchingGlob = pathGlobs.find(
1244
+ (glob6) => minimatch(relativePath, glob6)
1682
1245
  );
1683
- if (wxtConfig.analysis.enabled) {
1684
- config.plugins.push(bundleAnalysis(wxtConfig));
1685
- }
1686
- return config;
1687
- };
1688
- const getLibModeConfig = (entrypoint) => {
1689
- const entry = getRollupEntry(entrypoint);
1690
- const plugins = [
1691
- entrypointGroupGlobals(entrypoint)
1692
- ];
1693
- if (entrypoint.type === "content-script-style" || entrypoint.type === "unlisted-style") {
1694
- plugins.push(cssEntrypoints(entrypoint, wxtConfig));
1246
+ if (matchingGlob) {
1247
+ const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
1248
+ results.push({
1249
+ name,
1250
+ inputPath,
1251
+ type,
1252
+ skipped: wxt.config.filterEntrypoints != null && !wxt.config.filterEntrypoints.has(name)
1253
+ });
1695
1254
  }
1696
- const libMode = {
1697
- mode: wxtConfig.mode,
1698
- plugins,
1699
- build: {
1700
- lib: {
1701
- entry,
1702
- formats: ["iife"],
1703
- name: "_",
1704
- fileName: entrypoint.name
1705
- },
1706
- rollupOptions: {
1707
- output: {
1708
- // There's only a single output for this build, so we use the desired bundle path for the
1709
- // entry output (like "content-scripts/overlay.js")
1710
- entryFileNames: getEntrypointBundlePath(
1711
- entrypoint,
1712
- wxtConfig.outDir,
1713
- ".js"
1714
- ),
1715
- // Output content script CSS to `content-scripts/`, but all other scripts are written to
1716
- // `assets/`.
1717
- assetFileNames: ({ name }) => {
1718
- if (entrypoint.type === "content-script" && name?.endsWith("css")) {
1719
- return `content-scripts/${entrypoint.name}.[ext]`;
1720
- } else {
1721
- return `assets/${entrypoint.name}.[ext]`;
1722
- }
1255
+ return results;
1256
+ }, []);
1257
+ preventNoEntrypoints(entrypointInfos);
1258
+ preventDuplicateEntrypointNames(entrypointInfos);
1259
+ let hasBackground = false;
1260
+ const entrypoints = await Promise.all(
1261
+ entrypointInfos.map(async (info) => {
1262
+ const { type } = info;
1263
+ switch (type) {
1264
+ case "popup":
1265
+ return await getPopupEntrypoint(info);
1266
+ case "sidepanel":
1267
+ return await getSidepanelEntrypoint(info);
1268
+ case "options":
1269
+ return await getOptionsEntrypoint(info);
1270
+ case "background":
1271
+ hasBackground = true;
1272
+ return await getBackgroundEntrypoint(info);
1273
+ case "content-script":
1274
+ return await getContentScriptEntrypoint(info);
1275
+ case "unlisted-page":
1276
+ return await getUnlistedPageEntrypoint(info);
1277
+ case "unlisted-script":
1278
+ return await getUnlistedScriptEntrypoint(info);
1279
+ case "content-script-style":
1280
+ return {
1281
+ ...info,
1282
+ type,
1283
+ outputDir: resolve6(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
1284
+ options: {
1285
+ include: void 0,
1286
+ exclude: void 0
1723
1287
  }
1724
- }
1725
- }
1726
- },
1727
- define: {
1728
- // See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
1729
- "process.env.NODE_ENV": JSON.stringify(wxtConfig.mode)
1288
+ };
1289
+ default:
1290
+ return {
1291
+ ...info,
1292
+ type,
1293
+ outputDir: wxt.config.outDir,
1294
+ options: {
1295
+ include: void 0,
1296
+ exclude: void 0
1297
+ }
1298
+ };
1730
1299
  }
1731
- };
1732
- return libMode;
1733
- };
1734
- const getMultiPageConfig = (entrypoints) => {
1735
- const htmlEntrypoints = new Set(
1736
- entrypoints.filter(isHtmlEntrypoint).map((e) => e.name)
1300
+ })
1301
+ );
1302
+ if (wxt.config.command === "serve" && !hasBackground) {
1303
+ entrypoints.push(
1304
+ await getBackgroundEntrypoint({
1305
+ inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
1306
+ name: "background",
1307
+ type: "background",
1308
+ skipped: false
1309
+ })
1737
1310
  );
1738
- return {
1739
- mode: wxtConfig.mode,
1740
- plugins: [
1741
- multipageMove(entrypoints, wxtConfig),
1742
- entrypointGroupGlobals(entrypoints)
1743
- ],
1744
- build: {
1745
- rollupOptions: {
1746
- input: entrypoints.reduce((input, entry) => {
1747
- input[entry.name] = getRollupEntry(entry);
1748
- return input;
1749
- }, {}),
1750
- output: {
1751
- // Include a hash to prevent conflicts
1752
- chunkFileNames: "chunks/[name]-[hash].js",
1753
- entryFileNames: ({ name }) => {
1754
- if (htmlEntrypoints.has(name))
1755
- return "chunks/[name]-[hash].js";
1756
- return "[name].js";
1757
- },
1758
- // We can't control the "name", so we need a hash to prevent conflicts
1759
- assetFileNames: "assets/[name]-[hash].[ext]"
1760
- }
1761
- }
1311
+ }
1312
+ wxt.logger.debug("All entrypoints:", entrypoints);
1313
+ const skippedEntrypointNames = entrypointInfos.filter((item) => item.skipped).map((item) => item.name);
1314
+ if (skippedEntrypointNames.length) {
1315
+ wxt.logger.warn(
1316
+ `Filter excluded the following entrypoints:
1317
+ ${skippedEntrypointNames.map((item) => `${pc2.dim("-")} ${pc2.cyan(item)}`).join("\n")}`
1318
+ );
1319
+ }
1320
+ const targetEntrypoints = entrypoints.filter((entry) => {
1321
+ const { include, exclude } = entry.options;
1322
+ if (include?.length && exclude?.length) {
1323
+ wxt.logger.warn(
1324
+ `The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`
1325
+ );
1326
+ return false;
1327
+ }
1328
+ if (exclude?.length && !include?.length) {
1329
+ return !exclude.includes(wxt.config.browser);
1330
+ }
1331
+ if (include?.length && !exclude?.length) {
1332
+ return include.includes(wxt.config.browser);
1333
+ }
1334
+ if (skippedEntrypointNames.includes(entry.name)) {
1335
+ return false;
1336
+ }
1337
+ return true;
1338
+ });
1339
+ wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
1340
+ await wxt.hooks.callHook("entrypoints:resolved", wxt, targetEntrypoints);
1341
+ return targetEntrypoints;
1342
+ }
1343
+ function preventDuplicateEntrypointNames(files) {
1344
+ const namesToPaths = files.reduce(
1345
+ (map, { name, inputPath }) => {
1346
+ map[name] ??= [];
1347
+ map[name].push(inputPath);
1348
+ return map;
1349
+ },
1350
+ {}
1351
+ );
1352
+ const errorLines = Object.entries(namesToPaths).reduce(
1353
+ (lines, [name, absolutePaths]) => {
1354
+ if (absolutePaths.length > 1) {
1355
+ lines.push(`- ${name}`);
1356
+ absolutePaths.forEach((absolutePath) => {
1357
+ lines.push(` - ${relative3(wxt.config.root, absolutePath)}`);
1358
+ });
1762
1359
  }
1763
- };
1360
+ return lines;
1361
+ },
1362
+ []
1363
+ );
1364
+ if (errorLines.length > 0) {
1365
+ const errorContent = errorLines.join("\n");
1366
+ throw Error(
1367
+ `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.
1368
+
1369
+ ${errorContent}`
1370
+ );
1371
+ }
1372
+ }
1373
+ function preventNoEntrypoints(files) {
1374
+ if (files.length === 0) {
1375
+ throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
1376
+ }
1377
+ }
1378
+ async function getPopupEntrypoint(info) {
1379
+ const options = await getHtmlEntrypointOptions(
1380
+ info,
1381
+ {
1382
+ browserStyle: "browse_style",
1383
+ exclude: "exclude",
1384
+ include: "include",
1385
+ defaultIcon: "default_icon",
1386
+ defaultTitle: "default_title",
1387
+ mv2Key: "type"
1388
+ },
1389
+ {
1390
+ defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
1391
+ },
1392
+ {
1393
+ defaultTitle: (content) => content,
1394
+ mv2Key: (content) => content === "page_action" ? "page_action" : "browser_action"
1395
+ }
1396
+ );
1397
+ return {
1398
+ type: "popup",
1399
+ name: "popup",
1400
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1401
+ inputPath: info.inputPath,
1402
+ outputDir: wxt.config.outDir,
1403
+ skipped: info.skipped
1764
1404
  };
1765
- const getCssConfig = (entrypoint) => {
1766
- return {
1767
- mode: wxtConfig.mode,
1768
- plugins: [entrypointGroupGlobals(entrypoint)],
1769
- build: {
1770
- rollupOptions: {
1771
- input: {
1772
- [entrypoint.name]: entrypoint.inputPath
1773
- },
1774
- output: {
1775
- assetFileNames: () => {
1776
- if (entrypoint.type === "content-script-style") {
1777
- return `content-scripts/${entrypoint.name}.[ext]`;
1778
- } else {
1779
- return `assets/${entrypoint.name}.[ext]`;
1780
- }
1781
- }
1782
- }
1783
- }
1784
- }
1785
- };
1405
+ }
1406
+ async function getOptionsEntrypoint(info) {
1407
+ const options = await getHtmlEntrypointOptions(
1408
+ info,
1409
+ {
1410
+ browserStyle: "browse_style",
1411
+ chromeStyle: "chrome_style",
1412
+ exclude: "exclude",
1413
+ include: "include",
1414
+ openInTab: "open_in_tab"
1415
+ }
1416
+ );
1417
+ return {
1418
+ type: "options",
1419
+ name: "options",
1420
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1421
+ inputPath: info.inputPath,
1422
+ outputDir: wxt.config.outDir,
1423
+ skipped: info.skipped
1424
+ };
1425
+ }
1426
+ async function getUnlistedPageEntrypoint(info) {
1427
+ const options = await getHtmlEntrypointOptions(info, {
1428
+ exclude: "exclude",
1429
+ include: "include"
1430
+ });
1431
+ return {
1432
+ type: "unlisted-page",
1433
+ name: info.name,
1434
+ inputPath: info.inputPath,
1435
+ outputDir: wxt.config.outDir,
1436
+ options,
1437
+ skipped: info.skipped
1786
1438
  };
1439
+ }
1440
+ async function getUnlistedScriptEntrypoint({
1441
+ inputPath,
1442
+ name,
1443
+ skipped
1444
+ }) {
1445
+ const defaultExport = await importEntrypointFile(inputPath);
1446
+ if (defaultExport == null) {
1447
+ throw Error(
1448
+ `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`
1449
+ );
1450
+ }
1451
+ const { main: _, ...options } = defaultExport;
1452
+ return {
1453
+ type: "unlisted-script",
1454
+ name,
1455
+ inputPath,
1456
+ outputDir: wxt.config.outDir,
1457
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1458
+ skipped
1459
+ };
1460
+ }
1461
+ async function getBackgroundEntrypoint({
1462
+ inputPath,
1463
+ name,
1464
+ skipped
1465
+ }) {
1466
+ let options = {};
1467
+ if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
1468
+ const defaultExport = await importEntrypointFile(inputPath);
1469
+ if (defaultExport == null) {
1470
+ throw Error(
1471
+ `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`
1472
+ );
1473
+ }
1474
+ const { main: _, ...moduleOptions } = defaultExport;
1475
+ options = moduleOptions;
1476
+ }
1477
+ if (wxt.config.manifestVersion !== 3) {
1478
+ delete options.type;
1479
+ }
1480
+ return {
1481
+ type: "background",
1482
+ name,
1483
+ inputPath,
1484
+ outputDir: wxt.config.outDir,
1485
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1486
+ skipped
1487
+ };
1488
+ }
1489
+ async function getContentScriptEntrypoint({
1490
+ inputPath,
1491
+ name,
1492
+ skipped
1493
+ }) {
1494
+ const { main: _, ...options } = await importEntrypointFile(inputPath);
1495
+ if (options == null) {
1496
+ throw Error(
1497
+ `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`
1498
+ );
1499
+ }
1500
+ return {
1501
+ type: "content-script",
1502
+ name,
1503
+ inputPath,
1504
+ outputDir: resolve6(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
1505
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1506
+ skipped
1507
+ };
1508
+ }
1509
+ async function getSidepanelEntrypoint(info) {
1510
+ const options = await getHtmlEntrypointOptions(
1511
+ info,
1512
+ {
1513
+ browserStyle: "browse_style",
1514
+ exclude: "exclude",
1515
+ include: "include",
1516
+ defaultIcon: "default_icon",
1517
+ defaultTitle: "default_title",
1518
+ openAtInstall: "open_at_install"
1519
+ },
1520
+ {
1521
+ defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
1522
+ },
1523
+ {
1524
+ defaultTitle: (content) => content
1525
+ }
1526
+ );
1527
+ return {
1528
+ type: "sidepanel",
1529
+ name: info.name,
1530
+ options: resolvePerBrowserOptions(options, wxt.config.browser),
1531
+ inputPath: info.inputPath,
1532
+ outputDir: wxt.config.outDir,
1533
+ skipped: info.skipped
1534
+ };
1535
+ }
1536
+ async function getHtmlEntrypointOptions(info, keyMap, queries, parsers) {
1537
+ const content = await fs5.readFile(info.inputPath, "utf-8");
1538
+ const { document } = parseHTML2(content);
1539
+ const options = {};
1540
+ const defaultQuery = (manifestKey) => document.querySelector(`meta[name='manifest.${manifestKey}']`)?.getAttribute("content");
1541
+ Object.entries(keyMap).forEach(([_key, manifestKey]) => {
1542
+ const key = _key;
1543
+ const content2 = queries?.[key] ? queries[key](document, manifestKey) : defaultQuery(manifestKey);
1544
+ if (content2) {
1545
+ try {
1546
+ options[key] = (parsers?.[key] ?? JSON5.parse)(content2);
1547
+ } catch (err) {
1548
+ wxt.logger.fatal(
1549
+ `Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content2})`,
1550
+ err
1551
+ );
1552
+ }
1553
+ }
1554
+ });
1555
+ return options;
1556
+ }
1557
+ var PATH_GLOB_TO_TYPE_MAP = {
1558
+ "sandbox.html": "sandbox",
1559
+ "sandbox/index.html": "sandbox",
1560
+ "*.sandbox.html": "sandbox",
1561
+ "*.sandbox/index.html": "sandbox",
1562
+ "bookmarks.html": "bookmarks",
1563
+ "bookmarks/index.html": "bookmarks",
1564
+ "history.html": "history",
1565
+ "history/index.html": "history",
1566
+ "newtab.html": "newtab",
1567
+ "newtab/index.html": "newtab",
1568
+ "sidepanel.html": "sidepanel",
1569
+ "sidepanel/index.html": "sidepanel",
1570
+ "*.sidepanel.html": "sidepanel",
1571
+ "*.sidepanel/index.html": "sidepanel",
1572
+ "devtools.html": "devtools",
1573
+ "devtools/index.html": "devtools",
1574
+ "background.[jt]s": "background",
1575
+ "background/index.[jt]s": "background",
1576
+ [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: "background",
1577
+ "content.[jt]s?(x)": "content-script",
1578
+ "content/index.[jt]s?(x)": "content-script",
1579
+ "*.content.[jt]s?(x)": "content-script",
1580
+ "*.content/index.[jt]s?(x)": "content-script",
1581
+ [`content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
1582
+ [`*.content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
1583
+ [`content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
1584
+ [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style",
1585
+ "popup.html": "popup",
1586
+ "popup/index.html": "popup",
1587
+ "options.html": "options",
1588
+ "options/index.html": "options",
1589
+ "*.html": "unlisted-page",
1590
+ "*/index.html": "unlisted-page",
1591
+ "*.[jt]s?(x)": "unlisted-script",
1592
+ "*/index.[jt]s?(x)": "unlisted-script",
1593
+ [`*.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style",
1594
+ [`*/index.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style"
1595
+ };
1596
+ var CONTENT_SCRIPT_OUT_DIR = "content-scripts";
1597
+
1598
+ // src/core/utils/building/generate-wxt-dir.ts
1599
+ import { createUnimport as createUnimport2 } from "unimport";
1600
+ import fs6 from "fs-extra";
1601
+ import { relative as relative4, resolve as resolve7 } from "path";
1602
+ import path4 from "node:path";
1603
+
1604
+ // src/core/utils/i18n.ts
1605
+ var predefinedMessages = {
1606
+ "@@extension_id": {
1607
+ message: "<browser.runtime.id>",
1608
+ description: "The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file."
1609
+ },
1610
+ "@@ui_locale": {
1611
+ message: "<browser.i18n.getUiLocale()>",
1612
+ description: ""
1613
+ },
1614
+ "@@bidi_dir": {
1615
+ message: "<ltr|rtl>",
1616
+ description: 'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.'
1617
+ },
1618
+ "@@bidi_reversed_dir": {
1619
+ message: "<rtl|ltr>",
1620
+ description: `If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".`
1621
+ },
1622
+ "@@bidi_start_edge": {
1623
+ message: "<left|right>",
1624
+ description: `If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".`
1625
+ },
1626
+ "@@bidi_end_edge": {
1627
+ message: "<right|left>",
1628
+ description: `If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".`
1629
+ }
1630
+ };
1631
+ function parseI18nMessages(messagesJson) {
1632
+ return Object.entries({
1633
+ ...predefinedMessages,
1634
+ ...messagesJson
1635
+ }).map(([name, details]) => ({
1636
+ name,
1637
+ ...details
1638
+ }));
1639
+ }
1640
+
1641
+ // src/core/utils/building/generate-wxt-dir.ts
1642
+ async function generateTypesDir(entrypoints) {
1643
+ await fs6.ensureDir(wxt.config.typesDir);
1644
+ const references = [];
1645
+ if (wxt.config.imports !== false) {
1646
+ const unimport2 = createUnimport2(wxt.config.imports);
1647
+ references.push(await writeImportsDeclarationFile(unimport2));
1648
+ if (wxt.config.imports.eslintrc.enabled) {
1649
+ await writeImportsEslintFile(unimport2, wxt.config.imports);
1650
+ }
1651
+ }
1652
+ references.push(await writePathsDeclarationFile(entrypoints));
1653
+ references.push(await writeI18nDeclarationFile());
1654
+ references.push(await writeGlobalsDeclarationFile());
1655
+ const mainReference = await writeMainDeclarationFile(references);
1656
+ await writeTsConfigFile(mainReference);
1657
+ }
1658
+ async function writeImportsDeclarationFile(unimport2) {
1659
+ const filePath = resolve7(wxt.config.typesDir, "imports.d.ts");
1660
+ await unimport2.scanImportsFromDir(void 0, { cwd: wxt.config.srcDir });
1661
+ await writeFileIfDifferent(
1662
+ filePath,
1663
+ ["// Generated by wxt", await unimport2.generateTypeDeclarations()].join(
1664
+ "\n"
1665
+ ) + "\n"
1666
+ );
1667
+ return filePath;
1668
+ }
1669
+ async function writeImportsEslintFile(unimport2, options) {
1670
+ const globals2 = {};
1671
+ const eslintrc = { globals: globals2 };
1672
+ (await unimport2.getImports()).map((i) => i.as ?? i.name).filter(Boolean).sort().forEach((name) => {
1673
+ eslintrc.globals[name] = options.eslintrc.globalsPropValue;
1674
+ });
1675
+ await fs6.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
1676
+ }
1677
+ async function writePathsDeclarationFile(entrypoints) {
1678
+ const filePath = resolve7(wxt.config.typesDir, "paths.d.ts");
1679
+ const unions = entrypoints.map(
1680
+ (entry) => getEntrypointBundlePath(
1681
+ entry,
1682
+ wxt.config.outDir,
1683
+ isHtmlEntrypoint(entry) ? ".html" : ".js"
1684
+ )
1685
+ ).concat(await getPublicFiles()).map(normalizePath).map((path10) => ` | "/${path10}"`).sort().join("\n");
1686
+ const template = `// Generated by wxt
1687
+ import "wxt/browser";
1688
+
1689
+ declare module "wxt/browser" {
1690
+ export type PublicPath =
1691
+ {{ union }}
1692
+ type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
1693
+ export interface WxtRuntime extends Runtime.Static {
1694
+ getURL(path: PublicPath): string;
1695
+ getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
1696
+ }
1697
+ }
1698
+ `;
1699
+ await writeFileIfDifferent(
1700
+ filePath,
1701
+ template.replace("{{ union }}", unions || " | never")
1702
+ );
1703
+ return filePath;
1704
+ }
1705
+ async function writeI18nDeclarationFile() {
1706
+ const filePath = resolve7(wxt.config.typesDir, "i18n.d.ts");
1707
+ const defaultLocale = wxt.config.manifest.default_locale;
1708
+ const template = `// Generated by wxt
1709
+ import "wxt/browser";
1710
+
1711
+ declare module "wxt/browser" {
1712
+ /**
1713
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
1714
+ */
1715
+ interface GetMessageOptions {
1716
+ /**
1717
+ * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
1718
+ */
1719
+ escapeLt?: boolean
1720
+ }
1721
+
1722
+ export interface WxtI18n extends I18n.Static {
1723
+ {{ overrides }}
1724
+ }
1725
+ }
1726
+ `;
1727
+ let messages;
1728
+ if (defaultLocale) {
1729
+ const defaultLocalePath = path4.resolve(
1730
+ wxt.config.publicDir,
1731
+ "_locales",
1732
+ defaultLocale,
1733
+ "messages.json"
1734
+ );
1735
+ const content = JSON.parse(await fs6.readFile(defaultLocalePath, "utf-8"));
1736
+ messages = parseI18nMessages(content);
1737
+ } else {
1738
+ messages = parseI18nMessages({});
1739
+ }
1740
+ const overrides = messages.map((message) => {
1741
+ return ` /**
1742
+ * ${message.description || "No message description."}
1743
+ *
1744
+ * "${message.message}"
1745
+ */
1746
+ getMessage(
1747
+ messageName: "${message.name}",
1748
+ substitutions?: string | string[],
1749
+ options?: GetMessageOptions,
1750
+ ): string;`;
1751
+ });
1752
+ await writeFileIfDifferent(
1753
+ filePath,
1754
+ template.replace("{{ overrides }}", overrides.join("\n"))
1755
+ );
1756
+ return filePath;
1757
+ }
1758
+ async function writeGlobalsDeclarationFile() {
1759
+ const filePath = resolve7(wxt.config.typesDir, "globals.d.ts");
1760
+ const globals2 = [...getGlobals(wxt.config), ...getEntrypointGlobals("")];
1761
+ await writeFileIfDifferent(
1762
+ filePath,
1763
+ [
1764
+ "// Generated by wxt",
1765
+ "export {}",
1766
+ "interface ImportMetaEnv {",
1767
+ ...globals2.map((global) => ` readonly ${global.name}: ${global.type};`),
1768
+ "}",
1769
+ "interface ImportMeta {",
1770
+ " readonly env: ImportMetaEnv",
1771
+ "}"
1772
+ ].join("\n") + "\n"
1773
+ );
1774
+ return filePath;
1775
+ }
1776
+ async function writeMainDeclarationFile(references) {
1777
+ const dir = wxt.config.wxtDir;
1778
+ const filePath = resolve7(dir, "wxt.d.ts");
1779
+ await writeFileIfDifferent(
1780
+ filePath,
1781
+ [
1782
+ "// Generated by wxt",
1783
+ `/// <reference types="wxt/vite-builder-env" />`,
1784
+ ...references.map(
1785
+ (ref) => `/// <reference types="./${normalizePath(relative4(dir, ref))}" />`
1786
+ )
1787
+ ].join("\n") + "\n"
1788
+ );
1789
+ return filePath;
1790
+ }
1791
+ async function writeTsConfigFile(mainReference) {
1792
+ const dir = wxt.config.wxtDir;
1793
+ const getTsconfigPath = (path10) => normalizePath(relative4(dir, path10));
1794
+ const paths = Object.entries(wxt.config.alias).flatMap(([alias, absolutePath]) => {
1795
+ const aliasPath = getTsconfigPath(absolutePath);
1796
+ return [
1797
+ ` "${alias}": ["${aliasPath}"]`,
1798
+ ` "${alias}/*": ["${aliasPath}/*"]`
1799
+ ];
1800
+ }).join(",\n");
1801
+ await writeFileIfDifferent(
1802
+ resolve7(dir, "tsconfig.json"),
1803
+ `{
1804
+ "compilerOptions": {
1805
+ "target": "ESNext",
1806
+ "module": "ESNext",
1807
+ "moduleResolution": "Bundler",
1808
+ "noEmit": true,
1809
+ "esModuleInterop": true,
1810
+ "forceConsistentCasingInFileNames": true,
1811
+ "resolveJsonModule": true,
1812
+ "strict": true,
1813
+ "skipLibCheck": true,
1814
+ "paths": {
1815
+ ${paths}
1816
+ }
1817
+ },
1818
+ "include": [
1819
+ "${getTsconfigPath(wxt.config.root)}/**/*",
1820
+ "./${getTsconfigPath(mainReference)}"
1821
+ ],
1822
+ "exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
1823
+ }`
1824
+ );
1825
+ }
1826
+
1827
+ // src/core/utils/building/resolve-config.ts
1828
+ import { loadConfig } from "c12";
1829
+ import path5 from "node:path";
1830
+
1831
+ // src/core/utils/cache.ts
1832
+ import fs7, { ensureDir as ensureDir3 } from "fs-extra";
1833
+ import { dirname as dirname4, resolve as resolve8 } from "path";
1834
+ function createFsCache(wxtDir) {
1835
+ const getPath = (key) => resolve8(wxtDir, "cache", encodeURIComponent(key));
1787
1836
  return {
1788
- name: "Vite",
1789
- version: vite.version,
1790
- async build(group) {
1791
- let entryConfig;
1792
- if (Array.isArray(group))
1793
- entryConfig = getMultiPageConfig(group);
1794
- else if (group.inputPath.endsWith(".css"))
1795
- entryConfig = getCssConfig(group);
1796
- else
1797
- entryConfig = getLibModeConfig(group);
1798
- const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
1799
- const result = await vite.build(buildConfig);
1800
- return {
1801
- entrypoints: group,
1802
- chunks: getBuildOutputChunks(result)
1803
- };
1837
+ async set(key, value) {
1838
+ const path10 = getPath(key);
1839
+ await ensureDir3(dirname4(path10));
1840
+ await writeFileIfDifferent(path10, value);
1804
1841
  },
1805
- async createServer(info) {
1806
- const serverConfig = {
1807
- server: {
1808
- port: info.port,
1809
- strictPort: true,
1810
- host: info.hostname,
1811
- origin: info.origin
1812
- }
1813
- };
1814
- const baseConfig = await getBaseConfig();
1815
- const viteServer = await vite.createServer(
1816
- vite.mergeConfig(baseConfig, serverConfig)
1817
- );
1818
- const server = {
1819
- async listen() {
1820
- await viteServer.listen(info.port);
1821
- },
1822
- async close() {
1823
- await viteServer.close();
1824
- },
1825
- transformHtml(...args) {
1826
- return viteServer.transformIndexHtml(...args);
1827
- },
1828
- ws: {
1829
- send(message, payload) {
1830
- return viteServer.ws.send(message, payload);
1831
- },
1832
- on(message, cb) {
1833
- viteServer.ws.on(message, cb);
1834
- }
1835
- },
1836
- watcher: viteServer.watcher
1837
- };
1838
- return server;
1842
+ async get(key) {
1843
+ const path10 = getPath(key);
1844
+ try {
1845
+ return await fs7.readFile(path10, "utf-8");
1846
+ } catch {
1847
+ return void 0;
1848
+ }
1839
1849
  }
1840
1850
  };
1841
1851
  }
1842
- function getBuildOutputChunks(result) {
1843
- if ("on" in result)
1844
- throw Error("wxt does not support vite watch mode.");
1845
- if (Array.isArray(result))
1846
- return result.flatMap(({ output }) => output);
1847
- return result.output;
1848
- }
1849
- function getRollupEntry(entrypoint) {
1850
- let virtualEntrypointType;
1851
- switch (entrypoint.type) {
1852
- case "background":
1853
- case "unlisted-script":
1854
- virtualEntrypointType = entrypoint.type;
1855
- break;
1856
- case "content-script":
1857
- virtualEntrypointType = entrypoint.options.world === "MAIN" ? "content-script-main-world" : "content-script-isolated-world";
1858
- break;
1859
- }
1860
- return virtualEntrypointType ? `virtual:wxt-${virtualEntrypointType}?${entrypoint.inputPath}` : entrypoint.inputPath;
1861
- }
1862
1852
 
1863
1853
  // src/core/utils/building/resolve-config.ts
1854
+ import consola, { LogLevels } from "consola";
1864
1855
  import defu from "defu";
1865
1856
 
1866
1857
  // src/core/utils/package.ts
@@ -1883,7 +1874,7 @@ function isModuleInstalled(name) {
1883
1874
 
1884
1875
  // src/core/utils/building/resolve-config.ts
1885
1876
  import fs9 from "fs-extra";
1886
- async function resolveConfig(inlineConfig, command, server) {
1877
+ async function resolveConfig(inlineConfig, command) {
1887
1878
  let userConfig = {};
1888
1879
  let userConfigMetadata;
1889
1880
  if (inlineConfig.configFile !== false) {
@@ -1899,14 +1890,14 @@ async function resolveConfig(inlineConfig, command, server) {
1899
1890
  userConfig = loadedConfig ?? {};
1900
1891
  userConfigMetadata = metadata;
1901
1892
  }
1902
- const mergedConfig = mergeInlineConfig(inlineConfig, userConfig);
1893
+ const mergedConfig = await mergeInlineConfig(inlineConfig, userConfig);
1903
1894
  const debug = mergedConfig.debug ?? false;
1904
1895
  const logger = mergedConfig.logger ?? consola;
1905
1896
  if (debug)
1906
1897
  logger.level = LogLevels.debug;
1907
1898
  const browser = mergedConfig.browser ?? "chrome";
1908
1899
  const manifestVersion = mergedConfig.manifestVersion ?? (browser === "firefox" || browser === "safari" ? 2 : 3);
1909
- const mode = mergedConfig.mode ?? (command === "build" ? "production" : "development");
1900
+ const mode = mergedConfig.mode ?? COMMAND_MODES[command];
1910
1901
  const env = { browser, command, manifestVersion, mode };
1911
1902
  const root = path5.resolve(
1912
1903
  inlineConfig.root ?? userConfig.root ?? process.cwd()
@@ -1947,13 +1938,19 @@ async function resolveConfig(inlineConfig, command, server) {
1947
1938
  "~~": root
1948
1939
  }).map(([key, value]) => [key, path5.resolve(root, value)])
1949
1940
  );
1950
- const analysisOutputFile = path5.resolve(
1951
- root,
1952
- mergedConfig.analysis?.outputFile ?? "stats.html"
1953
- );
1954
- const analysisOutputDir = path5.dirname(analysisOutputFile);
1955
- const analysisOutputName = path5.parse(analysisOutputFile).name;
1956
- const finalConfig = {
1941
+ let devServerConfig;
1942
+ if (command === "serve") {
1943
+ let port = mergedConfig.dev?.server?.port;
1944
+ if (port == null || !isFinite(port)) {
1945
+ const { default: getPort, portNumbers } = await import("get-port");
1946
+ port = await getPort({ port: portNumbers(3e3, 3010) });
1947
+ }
1948
+ devServerConfig = {
1949
+ port,
1950
+ hostname: "localhost"
1951
+ };
1952
+ }
1953
+ return {
1957
1954
  browser,
1958
1955
  command,
1959
1956
  debug,
@@ -1975,109 +1972,47 @@ async function resolveConfig(inlineConfig, command, server) {
1975
1972
  srcDir,
1976
1973
  typesDir,
1977
1974
  wxtDir,
1978
- zip: resolveInternalZipConfig(root, mergedConfig),
1979
- transformManifest(manifest) {
1980
- userConfig.transformManifest?.(manifest);
1981
- inlineConfig.transformManifest?.(manifest);
1982
- },
1983
- analysis: {
1984
- enabled: mergedConfig.analysis?.enabled ?? false,
1985
- open: mergedConfig.analysis?.open ?? false,
1986
- template: mergedConfig.analysis?.template ?? "treemap",
1987
- outputFile: analysisOutputFile,
1988
- outputDir: analysisOutputDir,
1989
- outputName: analysisOutputName,
1990
- keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false
1991
- },
1975
+ zip: resolveZipConfig(root, mergedConfig),
1976
+ transformManifest: mergedConfig.transformManifest,
1977
+ analysis: resolveAnalysisConfig(root, mergedConfig),
1992
1978
  userConfigMetadata: userConfigMetadata ?? {},
1993
1979
  alias,
1994
- experimental: {
1995
- includeBrowserPolyfill: mergedConfig.experimental?.includeBrowserPolyfill ?? true
1996
- },
1997
- server,
1980
+ experimental: defu(mergedConfig.experimental, {
1981
+ includeBrowserPolyfill: true
1982
+ }),
1998
1983
  dev: {
1984
+ server: devServerConfig,
1999
1985
  reloadCommand
2000
1986
  },
2001
- hooks: mergedConfig.hooks ?? {}
2002
- };
2003
- const builder = await createViteBuilder(
2004
- inlineConfig,
2005
- userConfig,
2006
- finalConfig
2007
- );
2008
- return {
2009
- ...finalConfig,
2010
- builder
1987
+ hooks: mergedConfig.hooks ?? {},
1988
+ vite: mergedConfig.vite ?? (() => ({}))
2011
1989
  };
2012
1990
  }
2013
1991
  async function resolveManifestConfig(env, manifest) {
2014
1992
  return await (typeof manifest === "function" ? manifest(env) : manifest ?? {});
2015
1993
  }
2016
- function mergeInlineConfig(inlineConfig, userConfig) {
2017
- let imports;
2018
- if (inlineConfig.imports === false || userConfig.imports === false) {
2019
- imports = false;
2020
- } else if (userConfig.imports == null && inlineConfig.imports == null) {
2021
- imports = void 0;
2022
- } else {
2023
- imports = defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
2024
- }
1994
+ async function mergeInlineConfig(inlineConfig, userConfig) {
1995
+ const imports = inlineConfig.imports === false || userConfig.imports === false ? false : userConfig.imports == null && inlineConfig.imports == null ? void 0 : defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
2025
1996
  const manifest = async (env) => {
2026
1997
  const user = await resolveManifestConfig(env, userConfig.manifest);
2027
1998
  const inline = await resolveManifestConfig(env, inlineConfig.manifest);
2028
1999
  return defu(inline, user);
2029
2000
  };
2030
- const runner = defu(
2031
- inlineConfig.runner ?? {},
2032
- userConfig.runner ?? {}
2033
- );
2034
- const zip2 = defu(
2035
- inlineConfig.zip ?? {},
2036
- userConfig.zip ?? {}
2037
- );
2038
- const hooks = defu(
2039
- inlineConfig.hooks ?? {},
2040
- userConfig.hooks ?? {}
2041
- );
2001
+ const transformManifest = (manifest2) => {
2002
+ userConfig.transformManifest?.(manifest2);
2003
+ inlineConfig.transformManifest?.(manifest2);
2004
+ };
2005
+ const builderConfig = await mergeBuilderConfig(inlineConfig, userConfig);
2042
2006
  return {
2043
- root: inlineConfig.root ?? userConfig.root,
2044
- browser: inlineConfig.browser ?? userConfig.browser,
2045
- manifestVersion: inlineConfig.manifestVersion ?? userConfig.manifestVersion,
2046
- configFile: inlineConfig.configFile,
2047
- debug: inlineConfig.debug ?? userConfig.debug,
2048
- entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
2049
- filterEntrypoints: inlineConfig.filterEntrypoints ?? userConfig.filterEntrypoints,
2007
+ ...defu(inlineConfig, userConfig),
2008
+ // Custom merge values
2009
+ transformManifest,
2050
2010
  imports,
2051
- logger: inlineConfig.logger ?? userConfig.logger,
2052
2011
  manifest,
2053
- mode: inlineConfig.mode ?? userConfig.mode,
2054
- publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
2055
- runner,
2056
- srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
2057
- outDir: inlineConfig.outDir ?? userConfig.outDir,
2058
- zip: zip2,
2059
- analysis: {
2060
- ...userConfig.analysis,
2061
- ...inlineConfig.analysis
2062
- },
2063
- alias: {
2064
- ...userConfig.alias,
2065
- ...inlineConfig.alias
2066
- },
2067
- experimental: {
2068
- ...userConfig.experimental,
2069
- ...inlineConfig.experimental
2070
- },
2071
- vite: void 0,
2072
- transformManifest: void 0,
2073
- dev: {
2074
- ...userConfig.dev,
2075
- ...inlineConfig.dev
2076
- },
2077
- hooks
2012
+ ...builderConfig
2078
2013
  };
2079
2014
  }
2080
- function resolveInternalZipConfig(root, mergedConfig) {
2015
+ function resolveZipConfig(root, mergedConfig) {
2081
2016
  const downloadedPackagesDir = path5.resolve(root, ".wxt/local_modules");
2082
2017
  return {
2083
2018
  name: void 0,
@@ -2102,6 +2037,23 @@ function resolveInternalZipConfig(root, mergedConfig) {
2102
2037
  downloadedPackagesDir
2103
2038
  };
2104
2039
  }
2040
+ function resolveAnalysisConfig(root, mergedConfig) {
2041
+ const analysisOutputFile = path5.resolve(
2042
+ root,
2043
+ mergedConfig.analysis?.outputFile ?? "stats.html"
2044
+ );
2045
+ const analysisOutputDir = path5.dirname(analysisOutputFile);
2046
+ const analysisOutputName = path5.parse(analysisOutputFile).name;
2047
+ return {
2048
+ enabled: mergedConfig.analysis?.enabled ?? false,
2049
+ open: mergedConfig.analysis?.open ?? false,
2050
+ template: mergedConfig.analysis?.template ?? "treemap",
2051
+ outputFile: analysisOutputFile,
2052
+ outputDir: analysisOutputDir,
2053
+ outputName: analysisOutputName,
2054
+ keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false
2055
+ };
2056
+ }
2105
2057
  async function getUnimportOptions(wxtDir, logger, config) {
2106
2058
  if (config.imports === false)
2107
2059
  return false;
@@ -2154,6 +2106,23 @@ function logMissingDir(logger, name, expected) {
2154
2106
  )}`
2155
2107
  );
2156
2108
  }
2109
+ var COMMAND_MODES = {
2110
+ build: "production",
2111
+ serve: "development"
2112
+ };
2113
+ async function mergeBuilderConfig(inlineConfig, userConfig) {
2114
+ const vite = await import("vite").catch(() => void 0);
2115
+ if (vite) {
2116
+ return {
2117
+ vite: async (env) => {
2118
+ const resolvedInlineConfig = await inlineConfig.vite?.(env) ?? {};
2119
+ const resolvedUserConfig = await userConfig.vite?.(env) ?? {};
2120
+ return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig);
2121
+ }
2122
+ };
2123
+ }
2124
+ throw Error("Builder not found. Make sure vite is installed.");
2125
+ }
2157
2126
 
2158
2127
  // src/core/utils/building/group-entrypoints.ts
2159
2128
  function groupEntrypoints(entrypoints) {
@@ -2417,7 +2386,7 @@ function getChunkSortWeight(filename) {
2417
2386
  import pc4 from "picocolors";
2418
2387
 
2419
2388
  // package.json
2420
- var version = "0.17.8";
2389
+ var version = "0.17.10";
2421
2390
 
2422
2391
  // src/core/utils/log/printHeader.ts
2423
2392
  import { consola as consola2 } from "consola";
@@ -2595,7 +2564,7 @@ async function generateManifest(entrypoints, buildOutput) {
2595
2564
  addDevModeCsp(manifest);
2596
2565
  if (wxt.config.command === "serve")
2597
2566
  addDevModePermissions(manifest);
2598
- wxt.config.transformManifest(manifest);
2567
+ wxt.config.transformManifest?.(manifest);
2599
2568
  await wxt.hooks.callHook("build:manifestGenerated", wxt, manifest);
2600
2569
  if (wxt.config.manifestVersion === 2) {
2601
2570
  convertWebAccessibleResourcesToMv2(manifest);
@@ -2876,8 +2845,8 @@ function discoverIcons(buildOutput) {
2876
2845
  return icons.length > 0 ? Object.fromEntries(icons) : void 0;
2877
2846
  }
2878
2847
  function addDevModeCsp(manifest) {
2879
- const permission = `http://${wxt.config.server?.hostname ?? ""}/*`;
2880
- const allowedCsp = wxt.config.server?.origin ?? "http://localhost:*";
2848
+ const permission = `http://${wxt.server?.hostname ?? ""}/*`;
2849
+ const allowedCsp = wxt.server?.origin ?? "http://localhost:*";
2881
2850
  if (manifest.manifest_version === 3) {
2882
2851
  addHostPermission(manifest, permission);
2883
2852
  } else {
@@ -2890,7 +2859,7 @@ function addDevModeCsp(manifest) {
2890
2859
  ) : manifest.content_security_policy ?? "script-src 'self'; object-src 'self';"
2891
2860
  // default CSP for MV2
2892
2861
  );
2893
- if (wxt.config.server)
2862
+ if (wxt.server)
2894
2863
  csp.add("script-src", allowedCsp);
2895
2864
  if (manifest.manifest_version === 3) {
2896
2865
  manifest.content_security_policy ??= {};
@@ -3150,7 +3119,7 @@ async function internalBuild() {
3150
3119
  const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
3151
3120
  wxt.logger.info(
3152
3121
  `${verb} ${pc5.cyan(target)} for ${pc5.cyan(wxt.config.mode)} with ${pc5.green(
3153
- `${wxt.config.builder.name} ${wxt.config.builder.version}`
3122
+ `${wxt.builder.name} ${wxt.builder.version}`
3154
3123
  )}`
3155
3124
  );
3156
3125
  const startTime = Date.now();
@@ -3431,14 +3400,59 @@ import { Mutex } from "async-mutex";
3431
3400
  import pc7 from "picocolors";
3432
3401
  import { relative as relative10 } from "node:path";
3433
3402
  async function createServer(inlineConfig) {
3434
- const port = await getPort();
3435
- const hostname = "localhost";
3436
- const origin = `http://${hostname}:${port}`;
3437
- const serverInfo = {
3438
- port,
3439
- hostname,
3440
- origin
3441
- };
3403
+ await registerWxt("serve", inlineConfig, async (config) => {
3404
+ const { port, hostname } = config.dev.server;
3405
+ const serverInfo = {
3406
+ port,
3407
+ hostname,
3408
+ origin: `http://${hostname}:${port}`
3409
+ };
3410
+ const server2 = {
3411
+ ...serverInfo,
3412
+ get watcher() {
3413
+ return builderServer.watcher;
3414
+ },
3415
+ get ws() {
3416
+ return builderServer.ws;
3417
+ },
3418
+ currentOutput: void 0,
3419
+ async start() {
3420
+ await builderServer.listen();
3421
+ wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
3422
+ await buildAndOpenBrowser();
3423
+ },
3424
+ async stop() {
3425
+ await runner.closeBrowser();
3426
+ await builderServer.close();
3427
+ },
3428
+ async restart() {
3429
+ await closeAndRecreateRunner();
3430
+ await buildAndOpenBrowser();
3431
+ },
3432
+ transformHtml(url, html, originalUrl) {
3433
+ return builderServer.transformHtml(url, html, originalUrl);
3434
+ },
3435
+ reloadContentScript(payload) {
3436
+ server2.ws.send("wxt:reload-content-script", payload);
3437
+ },
3438
+ reloadPage(path10) {
3439
+ server2.ws.send("wxt:reload-page", path10);
3440
+ },
3441
+ reloadExtension() {
3442
+ server2.ws.send("wxt:reload-extension");
3443
+ },
3444
+ async restartBrowser() {
3445
+ await closeAndRecreateRunner();
3446
+ await runner.openBrowser();
3447
+ }
3448
+ };
3449
+ return server2;
3450
+ });
3451
+ const server = wxt.server;
3452
+ let [runner, builderServer] = await Promise.all([
3453
+ createExtensionRunner(),
3454
+ wxt.builder.createServer(server)
3455
+ ]);
3442
3456
  const buildAndOpenBrowser = async () => {
3443
3457
  server.currentOutput = await internalBuild();
3444
3458
  try {
@@ -3453,50 +3467,6 @@ async function createServer(inlineConfig) {
3453
3467
  await wxt.reloadConfig();
3454
3468
  runner = await createExtensionRunner();
3455
3469
  };
3456
- const server = {
3457
- ...serverInfo,
3458
- get watcher() {
3459
- return builderServer.watcher;
3460
- },
3461
- get ws() {
3462
- return builderServer.ws;
3463
- },
3464
- currentOutput: void 0,
3465
- async start() {
3466
- await builderServer.listen();
3467
- wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
3468
- await buildAndOpenBrowser();
3469
- },
3470
- async stop() {
3471
- await runner.closeBrowser();
3472
- await builderServer.close();
3473
- },
3474
- async restart() {
3475
- await closeAndRecreateRunner();
3476
- await buildAndOpenBrowser();
3477
- },
3478
- transformHtml(url, html, originalUrl) {
3479
- return builderServer.transformHtml(url, html, originalUrl);
3480
- },
3481
- reloadContentScript(payload) {
3482
- server.ws.send("wxt:reload-content-script", payload);
3483
- },
3484
- reloadPage(path10) {
3485
- server.ws.send("wxt:reload-page", path10);
3486
- },
3487
- reloadExtension() {
3488
- server.ws.send("wxt:reload-extension");
3489
- },
3490
- async restartBrowser() {
3491
- await closeAndRecreateRunner();
3492
- await runner.openBrowser();
3493
- }
3494
- };
3495
- await registerWxt("serve", inlineConfig, server);
3496
- let [runner, builderServer] = await Promise.all([
3497
- createExtensionRunner(),
3498
- wxt.config.builder.createServer(server)
3499
- ]);
3500
3470
  server.ws.on("wxt:background-initialized", () => {
3501
3471
  if (server.currentOutput == null)
3502
3472
  return;
@@ -3506,10 +3476,6 @@ async function createServer(inlineConfig) {
3506
3476
  server.watcher.on("all", reloadOnChange);
3507
3477
  return server;
3508
3478
  }
3509
- async function getPort() {
3510
- const { default: getPort2, portNumbers } = await import("get-port");
3511
- return await getPort2({ port: portNumbers(3e3, 3010) });
3512
- }
3513
3479
  function createFileReloader(server) {
3514
3480
  const fileChangedMutex = new Mutex();
3515
3481
  const changeQueue = [];
@@ -3784,7 +3750,7 @@ async function zip(config) {
3784
3750
  const applyTemplate = (template) => template.replaceAll("{{name}}", projectName).replaceAll("{{browser}}", wxt.config.browser).replaceAll(
3785
3751
  "{{version}}",
3786
3752
  output.manifest.version_name ?? output.manifest.version
3787
- ).replaceAll("{{manifestVersion}}", `mv${wxt.config.manifestVersion}`);
3753
+ ).replaceAll("{{mode}}", wxt.config.mode).replaceAll("{{manifestVersion}}", `mv${wxt.config.manifestVersion}`);
3788
3754
  await fs16.ensureDir(wxt.config.outBaseDir);
3789
3755
  const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
3790
3756
  const outZipPath = path9.resolve(wxt.config.outBaseDir, outZipFilename);
@@ -3952,7 +3918,7 @@ function isAliasedCommand(command) {
3952
3918
  // src/cli/commands.ts
3953
3919
  var cli = cac("wxt");
3954
3920
  cli.option("--debug", "enable debug mode");
3955
- cli.command("[root]", "start dev server").option("-c, --config <file>", "use specified config file").option("-m, --mode <mode>", "set env mode").option("-b, --browser <browser>", "specify a browser").option(
3921
+ cli.command("[root]", "start dev server").option("-c, --config <file>", "use specified config file").option("-m, --mode <mode>", "set env mode").option("-b, --browser <browser>", "specify a browser").option("-p, --port <port>", "specify a port for the dev server").option(
3956
3922
  "-e, --filter-entrypoint <entrypoint>",
3957
3923
  "only build specific entrypoints",
3958
3924
  {
@@ -3967,7 +3933,12 @@ cli.command("[root]", "start dev server").option("-c, --config <file>", "use spe
3967
3933
  manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : void 0,
3968
3934
  configFile: flags.config,
3969
3935
  debug: flags.debug,
3970
- filterEntrypoints: getArrayFromFlags(flags, "filterEntrypoint")
3936
+ filterEntrypoints: getArrayFromFlags(flags, "filterEntrypoint"),
3937
+ dev: flags.port == null ? void 0 : {
3938
+ server: {
3939
+ port: parseInt(flags.port)
3940
+ }
3941
+ }
3971
3942
  });
3972
3943
  await server.start();
3973
3944
  return { isOngoing: true };