chore: Abstract vite from WXT's core logic (#242)
This commit is contained in:
@@ -83,7 +83,7 @@ describe('User Config', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"3\\",\\"build\\",\\"production\\",\\"chrome\\"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.5.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.5.3",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.3",
|
||||
"esbuild": "^0.19.5",
|
||||
|
||||
Generated
+3
@@ -29,6 +29,9 @@ importers:
|
||||
cac:
|
||||
specifier: ^6.7.14
|
||||
version: 6.7.14
|
||||
chokidar:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
consola:
|
||||
specifier: ^3.2.3
|
||||
version: 3.2.3
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import type * as vite from 'vite';
|
||||
import {
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
UserConfig,
|
||||
WxtBuilder,
|
||||
WxtBuilderServer,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from './plugins';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
|
||||
export async function craeteViteBuilder(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
wxtConfig: Omit<InternalConfig, 'builder'>,
|
||||
): Promise<WxtBuilder> {
|
||||
const vite = await import('vite');
|
||||
|
||||
/**
|
||||
* Returns the base vite config shared by all builds based on the inline and user config.
|
||||
*/
|
||||
const getBaseConfig = async () => {
|
||||
const resolvedInlineConfig =
|
||||
(await inlineConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
const resolvedUserConfig = (await userConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
|
||||
const config: vite.InlineConfig = vite.mergeConfig(
|
||||
resolvedUserConfig,
|
||||
resolvedInlineConfig,
|
||||
);
|
||||
|
||||
config.root = wxtConfig.root;
|
||||
config.configFile = false;
|
||||
config.logLevel = 'warn';
|
||||
config.mode = wxtConfig.mode;
|
||||
|
||||
config.build ??= {};
|
||||
config.build.outDir = wxtConfig.outDir;
|
||||
config.build.emptyOutDir = false;
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig),
|
||||
wxtPlugins.unimport(wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('background', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('content-script', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('unlisted-script', wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig),
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
wxtPlugins.noopBackground(),
|
||||
wxtPlugins.globals(wxtConfig),
|
||||
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
|
||||
);
|
||||
if (wxtConfig.analysis.enabled) {
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis());
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode).
|
||||
*/
|
||||
const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
|
||||
const isVirtual = [
|
||||
'background',
|
||||
'content-script',
|
||||
'unlisted-script',
|
||||
].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const plugins: NonNullable<vite.UserConfig['plugins']> = [
|
||||
wxtPlugins.entrypointGroupGlobals(entrypoint),
|
||||
];
|
||||
if (
|
||||
entrypoint.type === 'content-script-style' ||
|
||||
entrypoint.type === 'unlisted-style'
|
||||
) {
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
|
||||
}
|
||||
|
||||
const libMode: vite.UserConfig = {
|
||||
mode: wxtConfig.mode,
|
||||
plugins,
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
name: '_',
|
||||
fileName: entrypoint.name,
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// There's only a single output for this build, so we use the desired bundle path for the
|
||||
// entry output (like "content-scripts/overlay.js")
|
||||
entryFileNames: getEntrypointBundlePath(
|
||||
entrypoint,
|
||||
wxtConfig.outDir,
|
||||
'.js',
|
||||
),
|
||||
// Output content script CSS to `content-scripts/`, but all other scripts are written to
|
||||
// `assets/`.
|
||||
assetFileNames: ({ name }) => {
|
||||
if (
|
||||
entrypoint.type === 'content-script' &&
|
||||
name?.endsWith('css')
|
||||
) {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(wxtConfig.mode),
|
||||
},
|
||||
};
|
||||
return libMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
*/
|
||||
const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
|
||||
return {
|
||||
mode: wxtConfig.mode,
|
||||
plugins: [
|
||||
wxtPlugins.multipageMove(entrypoints, wxtConfig),
|
||||
wxtPlugins.entrypointGroupGlobals(entrypoints),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
|
||||
input[entry.name] = entry.inputPath;
|
||||
return input;
|
||||
}, {}),
|
||||
output: {
|
||||
// Include a hash to prevent conflicts
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
// Include a hash to prevent conflicts
|
||||
entryFileNames: 'chunks/[name]-[hash].js',
|
||||
// We can't control the "name", so we need a hash to prevent conflicts
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'Vite',
|
||||
version: vite.version,
|
||||
async build(group) {
|
||||
const buildConfig = vite.mergeConfig(
|
||||
await getBaseConfig(),
|
||||
Array.isArray(group)
|
||||
? getMultiPageConfig(group)
|
||||
: getLibModeConfig(group),
|
||||
);
|
||||
const result = await vite.build(buildConfig);
|
||||
return {
|
||||
entrypoints: group,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
},
|
||||
async createServer(info) {
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
port: info.port,
|
||||
strictPort: true,
|
||||
host: info.hostname,
|
||||
origin: info.origin,
|
||||
},
|
||||
};
|
||||
const baseConfig = await getBaseConfig();
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(baseConfig, serverConfig),
|
||||
);
|
||||
|
||||
const server: WxtBuilderServer = {
|
||||
async listen() {
|
||||
await viteServer.listen(info.port);
|
||||
},
|
||||
transformHtml(...args) {
|
||||
return viteServer.transformIndexHtml(...args);
|
||||
},
|
||||
ws: {
|
||||
send(message, payload) {
|
||||
return viteServer.ws.send(message, payload);
|
||||
},
|
||||
on(message, cb) {
|
||||
viteServer.ws.on(message, cb);
|
||||
},
|
||||
},
|
||||
watcher: viteServer.watcher,
|
||||
};
|
||||
|
||||
return server;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildOutputChunks(
|
||||
result: Awaited<ReturnType<typeof vite.build>>,
|
||||
): BuildStepOutput['chunks'] {
|
||||
if ('on' in result) throw Error('wxt does not support vite watch mode.');
|
||||
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
|
||||
return result.output;
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
*/
|
||||
export function cssEntrypoints(
|
||||
entrypoint: Entrypoint,
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:css-entrypoint',
|
||||
+5
-7
@@ -2,7 +2,7 @@ import * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { getEntrypointName } from '~/core/utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, isAbsolute, relative, resolve } from 'path';
|
||||
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
||||
|
||||
// Cache the preamble script for all devHtmlPrerender plugins, not just one
|
||||
let reactRefreshPreamble = '';
|
||||
@@ -10,7 +10,9 @@ let reactRefreshPreamble = '';
|
||||
/**
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
*/
|
||||
export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
|
||||
export function devHtmlPrerender(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
config.root,
|
||||
@@ -86,11 +88,7 @@ export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
|
||||
const originalUrl = `${server.origin}${ctx.path}`;
|
||||
const name = getEntrypointName(config.entrypointsDir, ctx.filename);
|
||||
const url = `${server.origin}/${name}.html`;
|
||||
const serverHtml = await server.transformIndexHtml(
|
||||
url,
|
||||
html,
|
||||
originalUrl,
|
||||
);
|
||||
const serverHtml = await server.transformHtml(url, html, originalUrl);
|
||||
const { document } = parseHTML(serverHtml);
|
||||
|
||||
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
|
||||
+6
-7
@@ -4,20 +4,19 @@ import { InternalConfig } from '~/types';
|
||||
/**
|
||||
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
|
||||
*/
|
||||
export function devServerGlobals(internalConfig: InternalConfig): Plugin {
|
||||
export function devServerGlobals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Plugin {
|
||||
return {
|
||||
name: 'wxt:dev-server-globals',
|
||||
config() {
|
||||
if (internalConfig.server == null || internalConfig.command == 'build')
|
||||
return;
|
||||
if (config.server == null || config.command == 'build') return;
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(
|
||||
internalConfig.server.hostname,
|
||||
),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(internalConfig.server.port),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(config.server.hostname),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(config.server.port),
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
|
||||
* @example
|
||||
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
|
||||
*/
|
||||
export function download(config: InternalConfig): Plugin {
|
||||
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
|
||||
return {
|
||||
name: 'wxt:download',
|
||||
resolveId(id) {
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as vite from 'vite';
|
||||
import { EntrypointGroup } from '~/types';
|
||||
import { getEntrypointGlobals } from '~/core/utils/globals';
|
||||
|
||||
/**
|
||||
* Define a set of global variables specific to an entrypoint.
|
||||
*/
|
||||
export function entrypointGroupGlobals(
|
||||
entrypointGroup: EntrypointGroup,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:entrypoint-group-globals',
|
||||
config() {
|
||||
const define: vite.InlineConfig['define'] = {};
|
||||
let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;
|
||||
for (const global of getEntrypointGlobals(name)) {
|
||||
define[global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
return {
|
||||
define,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+3
-1
@@ -6,7 +6,9 @@ import * as vite from 'vite';
|
||||
* `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
|
||||
* virtual module.
|
||||
*/
|
||||
export function excludeBrowserPolyfill(config: InternalConfig): vite.Plugin {
|
||||
export function excludeBrowserPolyfill(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
|
||||
|
||||
return {
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
import { getGlobals } from '~/core/utils/globals';
|
||||
|
||||
export function globals(config: InternalConfig): vite.PluginOption {
|
||||
export function globals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:globals',
|
||||
config() {
|
||||
@@ -12,3 +12,4 @@ export * from './globals';
|
||||
export * from './webextensionPolyfillAlias';
|
||||
export * from './webextensionPolyfillInlineDeps';
|
||||
export * from './excludeBrowserPolyfill';
|
||||
export * from './entrypointGroupGlobals';
|
||||
+26
-2
@@ -1,6 +1,6 @@
|
||||
import * as vite from 'vite';
|
||||
import { Entrypoint, InternalConfig } from '~/types';
|
||||
import { dirname, extname, resolve } from 'node:path';
|
||||
import { dirname, extname, resolve, join } from 'node:path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import fs, { ensureDir } from 'fs-extra';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function multipageMove(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:multipage-move',
|
||||
@@ -67,6 +67,30 @@ export function multipageMove(
|
||||
delete bundle[oldBundlePath];
|
||||
bundle[newBundlePath] = renamedChunk;
|
||||
}
|
||||
|
||||
// Remove directories that were created
|
||||
// TODO: Optimize and only delete old path directories
|
||||
removeEmptyDirs(config.outDir);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all directories that are empty/
|
||||
*/
|
||||
export async function removeEmptyDirs(dir: string): Promise<void> {
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
const filePath = join(dir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
await removeEmptyDirs(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rmdir(dir);
|
||||
} catch {
|
||||
// noop on failure - this means the directory was not empty.
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,4 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
|
||||
/**
|
||||
* In dev mode, if there's not a background script listed, we need to add one.
|
||||
@@ -20,5 +21,3 @@ export function noopBackground(): Plugin {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(config: InternalConfig): vite.Plugin {
|
||||
export function tsconfigPaths(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
async config() {
|
||||
@@ -16,7 +16,9 @@ const ENABLED_EXTENSIONS = new Set([
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(config: InternalConfig): vite.PluginOption {
|
||||
export function unimport(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const options = getUnimportOptions(config);
|
||||
if (options === false) return [];
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function virtualEntrypoint(
|
||||
type: Entrypoint['type'],
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Plugin {
|
||||
const virtualId = `virtual:wxt-${type}?`;
|
||||
const resolvedVirtualId = `\0${virtualId}`;
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { InternalConfig } from '~/types';
|
||||
* This should only be used during tests.
|
||||
*/
|
||||
export function webextensionPolyfillAlias(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:webextension-polyfill-test-alias',
|
||||
+96
-114
@@ -3,10 +3,9 @@ import {
|
||||
EntrypointGroup,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
ServerInfo,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import * as vite from 'vite';
|
||||
import type { Scripting } from 'webextension-polyfill';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
getEntrypointOutputFile,
|
||||
@@ -38,35 +37,103 @@ import { relative } from 'node:path';
|
||||
* await server.start();
|
||||
*/
|
||||
export async function createServer(
|
||||
config?: InlineConfig,
|
||||
inlineConfig?: InlineConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const serverInfo = await getServerInfo();
|
||||
|
||||
const getLatestInternalConfig = async () => {
|
||||
return getInternalConfig(
|
||||
{
|
||||
...config,
|
||||
vite: () => serverInfo.viteServerConfig,
|
||||
},
|
||||
'serve',
|
||||
);
|
||||
const port = await getPort();
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverInfo: ServerInfo = {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
};
|
||||
|
||||
let internalConfig = await getLatestInternalConfig();
|
||||
const server = await setupServer(serverInfo, internalConfig);
|
||||
internalConfig.server = server;
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
watcher: undefined as any, // Filled out later down below
|
||||
ws: undefined as any, // Filled out later down below
|
||||
currentOutput: undefined as any, // Filled out later down below
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(contentScript) {
|
||||
server.ws.send('wxt:reload-content-script', contentScript);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
};
|
||||
|
||||
const getLatestConfig = () =>
|
||||
getInternalConfig(inlineConfig ?? {}, 'serve', server);
|
||||
let config = await getLatestConfig();
|
||||
|
||||
const [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(config),
|
||||
config.builder.createServer(server),
|
||||
]);
|
||||
|
||||
server.watcher = builderServer.watcher;
|
||||
server.ws = builderServer.ws;
|
||||
|
||||
// Register content scripts for the first time after the background starts up since they're not
|
||||
// listed in the manifest
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
reloadContentScripts(server.currentOutput.steps, config, server);
|
||||
});
|
||||
|
||||
// Listen for file changes and reload different parts of the extension accordingly
|
||||
const reloadOnChange = createFileReloader({
|
||||
server,
|
||||
getLatestConfig,
|
||||
updateConfig(newConfig) {
|
||||
config = newConfig;
|
||||
},
|
||||
});
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function getPort(): Promise<number> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
return await getPort({ port: portNumbers(3000, 3010) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function responsible for reloading different parts of the extension when a file
|
||||
* changes.
|
||||
*/
|
||||
function createFileReloader(options: {
|
||||
server: WxtDevServer;
|
||||
getLatestConfig: () => Promise<InternalConfig>;
|
||||
updateConfig: (config: InternalConfig) => void;
|
||||
}) {
|
||||
const { server, getLatestConfig, updateConfig } = options;
|
||||
const fileChangedMutex = new Mutex();
|
||||
const changeQueue: Array<[string, string]> = [];
|
||||
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
// Register content scripts for the first time since they're not listed in the manifest
|
||||
reloadContentScripts(server.currentOutput.steps, internalConfig, server);
|
||||
});
|
||||
return async (event: string, path: string) => {
|
||||
const config = await getLatestConfig();
|
||||
updateConfig(config);
|
||||
|
||||
server.watcher.on('all', async (event, path, _stats) => {
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(internalConfig.outBaseDir)) return;
|
||||
if (path.startsWith(config.outBaseDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
@@ -77,25 +144,23 @@ export async function createServer(
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
internalConfig.logger.info(
|
||||
config.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
|
||||
.map((file) => pc.dim(relative(internalConfig.root, file)))
|
||||
.map((file) => pc.dim(relative(config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => {
|
||||
return pc.cyan(
|
||||
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
|
||||
relative(config.outDir, getEntrypointOutputFile(entry, '')),
|
||||
);
|
||||
})
|
||||
.join(pc.dim(', '));
|
||||
|
||||
// Get latest config and Rebuild groups with changes
|
||||
internalConfig = await getLatestInternalConfig();
|
||||
internalConfig.server = server;
|
||||
// Rebuild entrypoints on change
|
||||
const { output: newOutput } = await rebuild(
|
||||
internalConfig,
|
||||
config,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
@@ -108,91 +173,15 @@ export async function createServer(
|
||||
server.reloadExtension();
|
||||
break;
|
||||
case 'html-reload':
|
||||
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
|
||||
reloadHtmlPages(changes.rebuildGroups, server, config);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, internalConfig, server);
|
||||
reloadContentScripts(changes.changedSteps, config, server);
|
||||
break;
|
||||
}
|
||||
consola.success(`Reloaded: ${rebuiltNames}`);
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function getServerInfo(): Promise<ServerInfo> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
const port = await getPort({ port: portNumbers(3000, 3010) });
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
origin,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
viteServerConfig: serverConfig,
|
||||
};
|
||||
}
|
||||
|
||||
async function setupServer(
|
||||
serverInfo: ServerInfo,
|
||||
config: InternalConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const runner = await createExtensionRunner(config);
|
||||
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(serverInfo, await config.vite(config.env)),
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
await viteServer.listen(server.port);
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
server.currentOutput = await internalBuild(config);
|
||||
await runner.openBrowser(config);
|
||||
};
|
||||
|
||||
const reloadExtension = () => {
|
||||
viteServer.ws.send('wxt:reload-extension');
|
||||
};
|
||||
const reloadPage = (path: string) => {
|
||||
// Can't use Vite's built-in "full-reload" event because it doesn't like our paths, it expects
|
||||
// paths ending in "/index.html"
|
||||
viteServer.ws.send('wxt:reload-page', path);
|
||||
};
|
||||
const reloadContentScript = (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => {
|
||||
viteServer.ws.send('wxt:reload-content-script', contentScript);
|
||||
};
|
||||
|
||||
const server: WxtDevServer = {
|
||||
...viteServer,
|
||||
start,
|
||||
currentOutput: {
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
name: '',
|
||||
version: '',
|
||||
},
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
},
|
||||
port: serverInfo.port,
|
||||
hostname: serverInfo.hostname,
|
||||
origin: serverInfo.origin,
|
||||
reloadExtension,
|
||||
reloadPage,
|
||||
reloadContentScript,
|
||||
};
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,10 +233,3 @@ function reloadHtmlPages(
|
||||
server.reloadPage(path);
|
||||
});
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
port: number;
|
||||
hostname: string;
|
||||
origin: string;
|
||||
viteServerConfig: vite.InlineConfig;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
fakeManifest,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeRollupOutputAsset,
|
||||
fakeRollupOutputChunk,
|
||||
fakeOutputAsset,
|
||||
fakeOutputChunk,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '~/types';
|
||||
|
||||
@@ -32,15 +32,11 @@ describe('Detect Dev Changes', () => {
|
||||
steps: [
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [fakeRollupOutputChunk(), fakeRollupOutputChunk()],
|
||||
chunks: [fakeOutputChunk(), fakeOutputChunk()],
|
||||
},
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
],
|
||||
chunks: [fakeOutputChunk(), fakeOutputChunk(), fakeOutputChunk()],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -57,10 +53,10 @@ describe('Detect Dev Changes', () => {
|
||||
'unknown',
|
||||
'/root/src/public/image.svg',
|
||||
];
|
||||
const asset1 = fakeRollupOutputAsset({
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
const asset2 = fakeRollupOutputAsset({
|
||||
const asset2 = fakeOutputAsset({
|
||||
fileName: 'some-other-image.svg',
|
||||
});
|
||||
const currentOutput: BuildOutput = {
|
||||
@@ -96,7 +92,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: contentScript,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -104,7 +100,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: background,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath, fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -150,7 +146,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: [htmlPage1, htmlPage2],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
@@ -158,7 +154,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: [htmlPage3],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -203,7 +199,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: script1,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
@@ -211,7 +207,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: script2,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -219,7 +215,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step3: BuildStepOutput = {
|
||||
entrypoints: script3,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [changedPath, fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from '~/core/vite-plugins';
|
||||
import { removeEmptyDirs } from '~/core/utils/fs';
|
||||
import { getPublicFiles } from '~/core/utils/fs';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { getEntrypointGlobals } from '~/core/utils/globals';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
|
||||
@@ -24,155 +18,18 @@ export async function buildEntrypoints(
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) +
|
||||
` ${[group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '))}`;
|
||||
|
||||
const step = Array.isArray(group)
|
||||
? await buildMultipleEntrypoints(group, config)
|
||||
: await buildSingleEntrypoint(group, config);
|
||||
steps.push(step);
|
||||
const groupNames = [group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '));
|
||||
spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNames}`;
|
||||
steps.push(await config.builder.build(group));
|
||||
}
|
||||
const publicAssets = await copyPublicDirectory(config);
|
||||
|
||||
// Remove any empty directories from moving outputs around
|
||||
await removeEmptyDirs(config.outDir);
|
||||
|
||||
return { publicAssets, steps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Vite's lib mode + IIFE format to bundle the entrypoint to a single file.
|
||||
*/
|
||||
async function buildSingleEntrypoint(
|
||||
entrypoint: Entrypoint,
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
|
||||
const isVirtual = [
|
||||
'background',
|
||||
'content-script',
|
||||
'unlisted-script',
|
||||
].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
|
||||
if (
|
||||
entrypoint.type === 'content-script-style' ||
|
||||
entrypoint.type === 'unlisted-style'
|
||||
) {
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
|
||||
}
|
||||
|
||||
const libMode: vite.UserConfig = {
|
||||
mode: config.mode,
|
||||
plugins,
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
name: '_',
|
||||
fileName: entrypoint.name,
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// There's only a single output for this build, so we use the desired bundle path for the
|
||||
// entry output (like "content-scripts/overlay.js")
|
||||
entryFileNames: getEntrypointBundlePath(
|
||||
entrypoint,
|
||||
config.outDir,
|
||||
'.js',
|
||||
),
|
||||
// Output content script CSS to `content-scripts/`, but all other scripts are written to
|
||||
// `assets/`.
|
||||
assetFileNames: ({ name }) => {
|
||||
if (entrypoint.type === 'content-script' && name?.endsWith('css')) {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(config.mode),
|
||||
},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
|
||||
libMode.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
const entryConfig = vite.mergeConfig(
|
||||
libMode,
|
||||
await config.vite(config.env),
|
||||
) as vite.InlineConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return {
|
||||
entrypoints: entrypoint,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Vite's multipage build to bundle all the entrypoints in a single step.
|
||||
*/
|
||||
async function buildMultipleEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
const multiPage: vite.UserConfig = {
|
||||
mode: config.mode,
|
||||
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
|
||||
input[entry.name] = entry.inputPath;
|
||||
return input;
|
||||
}, {}),
|
||||
output: {
|
||||
// Include a hash to prevent conflicts
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
// Include a hash to prevent conflicts
|
||||
entryFileNames: 'chunks/[name]-[hash].js',
|
||||
// We can't control the "name", so we need a hash to prevent conflicts
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, 'html')) {
|
||||
multiPage.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
|
||||
const entryConfig = vite.mergeConfig(
|
||||
multiPage,
|
||||
await config.vite(config.env),
|
||||
) as vite.UserConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return {
|
||||
entrypoints,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildOutputChunks(
|
||||
result: Awaited<ReturnType<typeof vite.build>>,
|
||||
): BuildStepOutput['chunks'] {
|
||||
if ('on' in result) throw Error('wxt does not support vite watch mode.');
|
||||
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
|
||||
return result.output;
|
||||
}
|
||||
|
||||
async function copyPublicDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
@@ -189,9 +46,6 @@ async function copyPublicDirectory(
|
||||
publicAssets.push({
|
||||
type: 'asset',
|
||||
fileName: file,
|
||||
name: file,
|
||||
needsCodeReference: false,
|
||||
source: await fs.readFile(srcPath),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
|
||||
@@ -105,9 +110,7 @@ function findEffectedSteps(
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = normalizePath(changedFile[1]);
|
||||
|
||||
const isChunkEffected = (
|
||||
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
|
||||
): boolean =>
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
// fileName is normalized, relative bundle path
|
||||
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
|
||||
@@ -176,4 +179,4 @@ interface ContentScriptReload extends RebuildChange {
|
||||
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
|
||||
* intermediate type.
|
||||
*/
|
||||
type DetectedChange = BuildStepOutput | vite.Rollup.OutputAsset;
|
||||
type DetectedChange = BuildStepOutput | OutputAsset;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
getEntrypointName,
|
||||
resolvePerBrowserOption,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/vite-plugins/noopBackground';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
|
||||
/**
|
||||
|
||||
@@ -156,7 +156,7 @@ async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
ConfigEnv,
|
||||
UserManifestFn,
|
||||
UserManifest,
|
||||
WxtViteConfig,
|
||||
ExtensionRunnerConfig,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { createFsCache } from '~/core/utils/cache';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import * as plugins from '~/core/vite-plugins';
|
||||
import { craeteViteBuilder } from '~/core/builders/vite';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
|
||||
@@ -27,6 +26,7 @@ import { NullablyRequired } from '../types';
|
||||
export async function getInternalConfig(
|
||||
inlineConfig: InlineConfig,
|
||||
command: 'build' | 'serve',
|
||||
server?: WxtDevServer,
|
||||
): Promise<InternalConfig> {
|
||||
// Load user config
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function getInternalConfig(
|
||||
}).map(([key, value]) => [key, path.resolve(root, value)]),
|
||||
);
|
||||
|
||||
const finalConfig: InternalConfig = {
|
||||
const finalConfig: Omit<InternalConfig, 'builder'> = {
|
||||
browser,
|
||||
command,
|
||||
debug,
|
||||
@@ -112,7 +112,6 @@ export async function getInternalConfig(
|
||||
runnerConfig,
|
||||
srcDir,
|
||||
typesDir,
|
||||
vite: () => ({}), // Real value added after this object is initialized.
|
||||
wxtDir,
|
||||
zip: resolveInternalZipConfig(root, mergedConfig),
|
||||
transformManifest(manifest) {
|
||||
@@ -129,12 +128,19 @@ export async function getInternalConfig(
|
||||
includeBrowserPolyfill:
|
||||
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
|
||||
},
|
||||
server,
|
||||
};
|
||||
|
||||
finalConfig.vite = (env) =>
|
||||
resolveInternalViteConfig(env, mergedConfig, finalConfig);
|
||||
const builder = await craeteViteBuilder(
|
||||
inlineConfig,
|
||||
userConfig,
|
||||
finalConfig,
|
||||
);
|
||||
|
||||
return finalConfig;
|
||||
return {
|
||||
...finalConfig,
|
||||
builder,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveManifestConfig(
|
||||
@@ -159,20 +165,12 @@ function mergeInlineConfig(
|
||||
} else if (userConfig.imports == null && inlineConfig.imports == null) {
|
||||
imports = undefined;
|
||||
} else {
|
||||
imports = vite.mergeConfig(
|
||||
userConfig.imports ?? {},
|
||||
inlineConfig.imports ?? {},
|
||||
);
|
||||
imports = defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
|
||||
}
|
||||
const manifest: UserManifestFn = async (env) => {
|
||||
const user = await resolveManifestConfig(env, userConfig.manifest);
|
||||
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
|
||||
return vite.mergeConfig(user, inline);
|
||||
};
|
||||
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
|
||||
const user = await userConfig.vite?.(env);
|
||||
const inline = await inlineConfig.vite?.(env);
|
||||
return vite.mergeConfig(user ?? {}, inline ?? {});
|
||||
return defu(inline, user);
|
||||
};
|
||||
const runner: InlineConfig['runner'] = defu(
|
||||
inlineConfig.runner ?? {},
|
||||
@@ -198,7 +196,6 @@ function mergeInlineConfig(
|
||||
runner,
|
||||
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
|
||||
outDir: inlineConfig.outDir ?? userConfig.outDir,
|
||||
vite: viteConfig,
|
||||
zip,
|
||||
analysis: {
|
||||
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
|
||||
@@ -213,6 +210,7 @@ function mergeInlineConfig(
|
||||
...userConfig.experimental,
|
||||
...inlineConfig.experimental,
|
||||
},
|
||||
vite: undefined,
|
||||
transformManifest: undefined,
|
||||
};
|
||||
}
|
||||
@@ -241,46 +239,3 @@ function resolveInternalZipConfig(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveInternalViteConfig(
|
||||
env: ConfigEnv,
|
||||
mergedConfig: InlineConfig,
|
||||
finalConfig: InternalConfig,
|
||||
) {
|
||||
const internalVite: vite.InlineConfig =
|
||||
(await mergedConfig.vite?.(env)) ?? {};
|
||||
|
||||
internalVite.root = finalConfig.root;
|
||||
internalVite.configFile = false;
|
||||
internalVite.logLevel = 'warn';
|
||||
internalVite.mode = env.mode;
|
||||
|
||||
internalVite.build ??= {};
|
||||
internalVite.build.outDir = finalConfig.outDir;
|
||||
internalVite.build.emptyOutDir = false;
|
||||
|
||||
internalVite.plugins ??= [];
|
||||
internalVite.plugins.push(plugins.download(finalConfig));
|
||||
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
|
||||
internalVite.plugins.push(plugins.unimport(finalConfig));
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('background', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('content-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('unlisted-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
internalVite.plugins.push(plugins.noopBackground());
|
||||
if (finalConfig.analysis.enabled) {
|
||||
internalVite.plugins.push(plugins.bundleAnalysis());
|
||||
}
|
||||
internalVite.plugins.push(plugins.globals(finalConfig));
|
||||
|
||||
internalVite.plugins.push(plugins.excludeBrowserPolyfill(finalConfig));
|
||||
|
||||
return internalVite;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { findEntrypoints } from './find-entrypoints';
|
||||
import { InternalConfig, BuildOutput } from '~/types';
|
||||
import pc from 'picocolors';
|
||||
import * as vite from 'vite';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
@@ -27,7 +26,7 @@ export async function internalBuild(
|
||||
const target = `${config.browser}-mv${config.manifestVersion}`;
|
||||
config.logger.info(
|
||||
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
|
||||
`Vite ${vite.version}`,
|
||||
`${config.builder.name} ${config.builder.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Module ID used to build the background in dev mode if the extension doesn't include a background
|
||||
* script/service worker.
|
||||
*/
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
@@ -2,7 +2,6 @@ import { InternalConfig } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from './paths';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Only write the contents to a file if it results in a change. This prevents unnecessary file
|
||||
@@ -36,23 +35,3 @@ export async function getPublicFiles(
|
||||
const files = await glob('**/*', { cwd: config.publicDir });
|
||||
return files.map(unnormalizePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all directories that are empty/
|
||||
*/
|
||||
export async function removeEmptyDirs(dir: string): Promise<void> {
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
await removeEmptyDirs(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rmdir(dir);
|
||||
} catch {
|
||||
// noop on failure - this means the directory was not empty.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
|
||||
export function getGlobals(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Array<{ name: string; value: any; type: string }> {
|
||||
return [
|
||||
{
|
||||
@@ -47,10 +47,7 @@ export function getGlobals(
|
||||
];
|
||||
}
|
||||
|
||||
export function getEntrypointGlobals(
|
||||
config: InternalConfig,
|
||||
entrypointName: string,
|
||||
) {
|
||||
export function getEntrypointGlobals(entrypointName: string) {
|
||||
return [
|
||||
{
|
||||
name: surroundInUnderscore('ENTRYPOINT'),
|
||||
|
||||
@@ -44,9 +44,6 @@ export async function writeManifest(
|
||||
output.publicAssets.unshift({
|
||||
type: 'asset',
|
||||
fileName: 'manifest.json',
|
||||
name: 'manifest',
|
||||
needsCodeReference: false,
|
||||
source: str,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
|
||||
*/
|
||||
export async function fetchCached(
|
||||
url: string,
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Promise<string> {
|
||||
let content: string = '';
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { resolve } from 'path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import merge from 'lodash.merge';
|
||||
import { Rollup } from 'vite';
|
||||
import type { Manifest } from 'webextension-polyfill';
|
||||
import {
|
||||
FsCache,
|
||||
@@ -15,6 +14,9 @@ import {
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
OutputChunk,
|
||||
OutputFile,
|
||||
OutputAsset,
|
||||
} from '~/types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
@@ -140,49 +142,19 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeRollupOutputChunk = fakeObjectCreator<Rollup.OutputChunk>(
|
||||
() => ({
|
||||
type: 'chunk',
|
||||
code: '',
|
||||
dynamicImports: [],
|
||||
exports: [],
|
||||
facadeModuleId: faker.helpers.arrayElement([null, fakeFile()]),
|
||||
fileName: faker.string.alphanumeric(),
|
||||
implicitlyLoadedBefore: [],
|
||||
importedBindings: {},
|
||||
imports: [],
|
||||
isDynamicEntry: faker.datatype.boolean(),
|
||||
isEntry: faker.datatype.boolean(),
|
||||
isImplicitEntry: faker.datatype.boolean(),
|
||||
map: null,
|
||||
moduleIds: [],
|
||||
modules: {},
|
||||
name: faker.string.alpha(),
|
||||
referencedFiles: [],
|
||||
viteMetadata: {
|
||||
importedAssets: new Set(),
|
||||
importedCss: new Set(),
|
||||
},
|
||||
preliminaryFileName: faker.string.alphanumeric(),
|
||||
sourcemapFileName: null,
|
||||
}),
|
||||
);
|
||||
export const fakeOutputChunk = fakeObjectCreator<OutputChunk>(() => ({
|
||||
type: 'chunk',
|
||||
fileName: faker.string.alphanumeric(),
|
||||
moduleIds: [],
|
||||
}));
|
||||
|
||||
export const fakeRollupOutputAsset = fakeObjectCreator<Rollup.OutputAsset>(
|
||||
() => ({
|
||||
type: 'asset',
|
||||
fileName: fakeFileName(),
|
||||
name: faker.string.alpha(),
|
||||
needsCodeReference: faker.datatype.boolean(),
|
||||
source: '',
|
||||
}),
|
||||
);
|
||||
export const fakeOutputAsset = fakeObjectCreator<OutputAsset>(() => ({
|
||||
type: 'asset',
|
||||
fileName: fakeFileName(),
|
||||
}));
|
||||
|
||||
export function fakeRollupOutput(): Rollup.OutputAsset | Rollup.OutputChunk {
|
||||
return faker.helpers.arrayElement([
|
||||
fakeRollupOutputAsset(),
|
||||
fakeRollupOutputChunk(),
|
||||
]);
|
||||
export function fakeOutputFile(): OutputFile {
|
||||
return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]);
|
||||
}
|
||||
|
||||
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
|
||||
@@ -228,7 +200,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
debug: faker.datatype.boolean(),
|
||||
srcDir: fakeDir(),
|
||||
typesDir: fakeDir(),
|
||||
vite: () => ({}),
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
analysis: {
|
||||
@@ -248,5 +219,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
experimental: {
|
||||
includeBrowserPolyfill: true,
|
||||
},
|
||||
builder: mock(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { mergeConfig } from 'vite';
|
||||
import { defu } from 'defu';
|
||||
|
||||
export function getUnimportOptions(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Partial<UnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
@@ -23,8 +23,5 @@ export function getUnimportOptions(
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
return mergeConfig(
|
||||
defaultOptions,
|
||||
config.imports,
|
||||
) as Partial<UnimportOptions>;
|
||||
return defu(config.imports, defaultOptions);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
globals,
|
||||
webextensionPolyfillAlias,
|
||||
webextensionPolyfillInlineDeps,
|
||||
} from '../core/vite-plugins';
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { getInternalConfig } from '~/core/utils/building';
|
||||
import { InlineConfig } from '../types';
|
||||
|
||||
|
||||
+130
-38
@@ -4,6 +4,7 @@ import { UnimportOptions } from 'unimport';
|
||||
import { LogLevel } from 'consola';
|
||||
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
@@ -89,17 +90,6 @@ export interface InlineConfig {
|
||||
* consola
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
/**
|
||||
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
* object or promise.
|
||||
@@ -200,10 +190,6 @@ export interface InlineConfig {
|
||||
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
|
||||
* in the root `tsconfig.json` if you want to add new paths.
|
||||
*
|
||||
* Passed into Vite's
|
||||
* [`resolve.alias`](https://vitejs.dev/config/shared-options.html#resolve-alias) option and used
|
||||
* to generate the `.wxt/tsconfig.json`.
|
||||
*
|
||||
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
|
||||
*
|
||||
* @example
|
||||
@@ -239,43 +225,83 @@ export interface InlineConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
extends Omit<vite.InlineConfig, 'root' | 'configFile' | 'mode' | 'build'> {
|
||||
build?: Omit<vite.BuildOptions, 'outDir'>;
|
||||
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
}
|
||||
|
||||
// TODO: Move into @wxt/vite-builder
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
|
||||
export interface BuildOutput {
|
||||
manifest: Manifest.WebExtensionManifest;
|
||||
publicAssets: vite.Rollup.OutputAsset[];
|
||||
publicAssets: OutputAsset[];
|
||||
steps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
export type OutputFile = OutputChunk | OutputAsset;
|
||||
|
||||
export interface OutputChunk {
|
||||
type: 'chunk';
|
||||
/**
|
||||
* Relative, normalized path relative to the output directory.
|
||||
*
|
||||
* Ex: "content-scripts/overlay.js"
|
||||
*/
|
||||
fileName: string;
|
||||
/**
|
||||
* Absolute, normalized paths to all dependencies this chunk relies on.
|
||||
*/
|
||||
moduleIds: string[];
|
||||
}
|
||||
|
||||
export interface OutputAsset {
|
||||
type: 'asset';
|
||||
/**
|
||||
* Relative, normalized path relative to the output directory.
|
||||
*
|
||||
* Ex: "icons/16.png"
|
||||
*/
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface BuildStepOutput {
|
||||
entrypoints: EntrypointGroup;
|
||||
chunks: (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
|
||||
chunks: OutputFile[];
|
||||
}
|
||||
|
||||
export interface WxtDevServer extends vite.ViteDevServer {
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
origin: string;
|
||||
export interface WxtDevServer
|
||||
extends Omit<WxtBuilderServer, 'listen'>,
|
||||
ServerInfo {
|
||||
/**
|
||||
* Stores the current build output of the server.
|
||||
*/
|
||||
currentOutput: BuildOutput;
|
||||
/**
|
||||
* Start the server on the first open port.
|
||||
* Start the server.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
transformHtml(
|
||||
url: string,
|
||||
html: string,
|
||||
originalUrl?: string | undefined,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Tell the extension to reload by running `browser.runtime.reload`.
|
||||
*/
|
||||
@@ -596,7 +622,73 @@ export interface ExtensionRunnerConfig {
|
||||
startUrls?: string[];
|
||||
}
|
||||
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
export interface WxtBuilder {
|
||||
/**
|
||||
* Name of tool used to build. Ex: "Vite" or "Webpack".
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Version of tool used to build. Ex: "5.0.2"
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
|
||||
* build process.
|
||||
*/
|
||||
build(group: EntrypointGroup): Promise<BuildStepOutput>;
|
||||
/**
|
||||
* Start a dev server at the provided port.
|
||||
*/
|
||||
createServer(info: ServerInfo): Promise<WxtBuilderServer>;
|
||||
}
|
||||
|
||||
export interface WxtBuilderServer {
|
||||
/**
|
||||
* Start the server.
|
||||
*/
|
||||
listen(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
transformHtml(
|
||||
url: string,
|
||||
html: string,
|
||||
originalUrl?: string | undefined,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* The web socket server used to communicate with the extension.
|
||||
*/
|
||||
ws: {
|
||||
/**
|
||||
* Send a message via the server's websocket, with an optional payload.
|
||||
*
|
||||
* @example
|
||||
* ws.send("wxt:reload-extension");
|
||||
* ws.send("wxt:reload-content-script", { ... });
|
||||
*/
|
||||
send(message: string, payload?: any): void;
|
||||
/**
|
||||
* Listen for messages over the server's websocket.
|
||||
*/
|
||||
on(message: string, cb: (payload: any) => void): void;
|
||||
};
|
||||
/**
|
||||
* Chokidar file watcher instance.
|
||||
*/
|
||||
watcher: FSWatcher;
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
origin: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
WxtDevServer,
|
||||
Logger,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
ExtensionRunnerConfig,
|
||||
ConfigEnv,
|
||||
UserConfig,
|
||||
WxtBuilder,
|
||||
} from './external';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { ResolvedConfig } from 'c12';
|
||||
@@ -31,7 +31,6 @@ export interface InternalConfig {
|
||||
env: ConfigEnv;
|
||||
logger: Logger;
|
||||
imports: false | Partial<UnimportOptions>;
|
||||
vite: (env: ConfigEnv) => Promise<vite.InlineConfig> | vite.InlineConfig;
|
||||
manifest: UserManifest;
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
@@ -56,6 +55,7 @@ export interface InternalConfig {
|
||||
experimental: {
|
||||
includeBrowserPolyfill: boolean;
|
||||
};
|
||||
builder: WxtBuilder;
|
||||
}
|
||||
|
||||
export interface FsCache {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { defineWorkspace } from 'vitest/config';
|
||||
import fs from 'fs-extra';
|
||||
import pc from 'picocolors';
|
||||
import { Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
const seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
console.info('Test seed: ' + pc.cyan(seed));
|
||||
|
||||
Reference in New Issue
Block a user