diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts
index e8d2e94b..36261f11 100644
--- a/packages/wxt/e2e/tests/auto-imports.test.ts
+++ b/packages/wxt/e2e/tests/auto-imports.test.ts
@@ -47,10 +47,10 @@ describe('Auto Imports', () => {
----------------------------------------
// Generated by wxt
///
- ///
///
///
///
+ ///
"
`);
});
diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts
index 0cd78476..cdf80090 100644
--- a/packages/wxt/e2e/tests/typescript-project.test.ts
+++ b/packages/wxt/e2e/tests/typescript-project.test.ts
@@ -233,10 +233,10 @@ describe('TypeScript Project', () => {
----------------------------------------
// Generated by wxt
///
- ///
///
///
///
+ ///
"
`);
});
diff --git a/packages/wxt/src/builtin-modules/index.ts b/packages/wxt/src/builtin-modules/index.ts
new file mode 100644
index 00000000..de85b98a
--- /dev/null
+++ b/packages/wxt/src/builtin-modules/index.ts
@@ -0,0 +1,4 @@
+import { WxtModule } from '~/types';
+import unimport from './unimport';
+
+export const builtinModules: WxtModule[] = [unimport];
diff --git a/packages/wxt/src/builtin-modules/unimport.ts b/packages/wxt/src/builtin-modules/unimport.ts
new file mode 100644
index 00000000..edc5cac6
--- /dev/null
+++ b/packages/wxt/src/builtin-modules/unimport.ts
@@ -0,0 +1,122 @@
+import { addViteConfig, defineWxtModule } from '~/modules';
+import type {
+ EslintGlobalsPropValue,
+ WxtDirFileEntry,
+ WxtModule,
+ WxtResolvedUnimportOptions,
+} from '~/types';
+import { type Unimport, createUnimport } from 'unimport';
+import { Plugin } from 'vite';
+import { extname } from 'node:path';
+
+export default defineWxtModule({
+ name: 'wxt:built-in:unimport',
+ setup(wxt) {
+ const options = wxt.config.imports;
+ if (options === false) return;
+
+ let unimport: Unimport;
+
+ // Add user module imports to config
+ wxt.hooks.hook('ready', () => {
+ const addModuleImports = (module: WxtModule) => {
+ if (!module.imports) return;
+
+ options.imports ??= [];
+ options.imports.push(...module.imports);
+ };
+
+ wxt.config.builtinModules.forEach(addModuleImports);
+ wxt.config.userModules.forEach(addModuleImports);
+ });
+
+ // Create unimport instance after READY so any modifications to the config
+ // inside "ready" are applied.
+ wxt.hooks.afterEach((event) => {
+ if (event.name === 'ready') {
+ unimport = createUnimport(options);
+ }
+ });
+
+ // Generate types
+ wxt.hooks.hook('prepare:types', async (_, entries) => {
+ // Update cache before each rebuild
+ await unimport.init();
+
+ entries.push(await getImportsDeclarationEntry(unimport));
+
+ if (!options.eslintrc.enabled) return;
+ entries.push(await getImportsEslintEntry(unimport, options));
+ });
+
+ // Add vite plugin
+ addViteConfig(wxt, () => ({
+ plugins: [vitePlugin(unimport)],
+ }));
+ },
+});
+
+export function vitePlugin(unimport: Unimport): Plugin {
+ const ENABLED_EXTENSIONS = new Set([
+ '.js',
+ '.jsx',
+ '.ts',
+ '.tsx',
+ '.vue',
+ '.svelte',
+ ]);
+ return {
+ name: 'wxt:unimport',
+ async transform(code, id) {
+ // Don't transform dependencies
+ if (id.includes('node_modules')) return;
+
+ // Don't transform non-js files
+ if (!ENABLED_EXTENSIONS.has(extname(id))) return;
+
+ const injected = await unimport.injectImports(code, id);
+ return {
+ code: injected.code,
+ map: injected.s.generateMap({ hires: 'boundary', source: id }),
+ };
+ },
+ };
+}
+
+async function getImportsDeclarationEntry(
+ unimport: Unimport,
+): Promise {
+ // Load project imports into unimport memory so they are output via generateTypeDeclarations
+ await unimport.init();
+
+ return {
+ path: 'types/imports.d.ts',
+ text: [
+ '// Generated by wxt',
+ await unimport.generateTypeDeclarations(),
+ '',
+ ].join('\n'),
+ tsReference: true,
+ };
+}
+
+async function getImportsEslintEntry(
+ unimport: Unimport,
+ options: WxtResolvedUnimportOptions,
+): Promise {
+ const globals: Record = {};
+ const eslintrc = { globals };
+
+ (await unimport.getImports())
+ .map((i) => i.as ?? i.name)
+ .filter(Boolean)
+ .sort()
+ .forEach((name) => {
+ eslintrc.globals[name] = options.eslintrc.globalsPropValue;
+ });
+
+ return {
+ path: options.eslintrc.filePath,
+ text: JSON.stringify(eslintrc, null, 2) + '\n',
+ };
+}
diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts
index 800b750e..e8b13de2 100644
--- a/packages/wxt/src/core/builders/vite/index.ts
+++ b/packages/wxt/src/core/builders/vite/index.ts
@@ -61,7 +61,6 @@ export async function createViteBuilder(
config.plugins.push(
wxtPlugins.download(wxtConfig),
wxtPlugins.devHtmlPrerender(wxtConfig, server),
- wxtPlugins.unimport(wxtConfig),
wxtPlugins.resolveVirtualModules(wxtConfig),
wxtPlugins.devServerGlobals(wxtConfig, server),
wxtPlugins.tsconfigPaths(wxtConfig),
diff --git a/packages/wxt/src/core/builders/vite/plugins/index.ts b/packages/wxt/src/core/builders/vite/plugins/index.ts
index a81926b8..de578d01 100644
--- a/packages/wxt/src/core/builders/vite/plugins/index.ts
+++ b/packages/wxt/src/core/builders/vite/plugins/index.ts
@@ -2,7 +2,6 @@ export * from './devHtmlPrerender';
export * from './devServerGlobals';
export * from './download';
export * from './multipageMove';
-export * from './unimport';
export * from './resolveVirtualModules';
export * from './tsconfigPaths';
export * from './noopBackground';
diff --git a/packages/wxt/src/core/builders/vite/plugins/unimport.ts b/packages/wxt/src/core/builders/vite/plugins/unimport.ts
deleted file mode 100644
index fa9c7eac..00000000
--- a/packages/wxt/src/core/builders/vite/plugins/unimport.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createUnimport } from 'unimport';
-import { ResolvedConfig } from '~/types';
-import type * as vite from 'vite';
-import { extname } from 'path';
-
-const ENABLED_EXTENSIONS = new Set([
- '.js',
- '.jsx',
- '.ts',
- '.tsx',
- '.vue',
- '.svelte',
-]);
-
-/**
- * Inject any global imports defined by unimport
- */
-export function unimport(config: ResolvedConfig): vite.PluginOption {
- const options = config.imports;
- if (options === false) return [];
-
- const unimport = createUnimport(options);
-
- return {
- name: 'wxt:unimport',
- async config() {
- await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
- },
- async transform(code, id) {
- // Don't transform dependencies
- if (id.includes('node_modules')) return;
-
- // Don't transform non-js files
- if (!ENABLED_EXTENSIONS.has(extname(id))) return;
-
- const injected = await unimport.injectImports(code, id);
- return {
- code: injected.code,
- map: injected.s.generateMap({ hires: 'boundary', source: id }),
- };
- },
- };
-}
diff --git a/packages/wxt/src/core/utils/building/generate-wxt-dir.ts b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts
index 0e230c4c..a214718a 100644
--- a/packages/wxt/src/core/utils/building/generate-wxt-dir.ts
+++ b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts
@@ -1,11 +1,4 @@
-import { Unimport, createUnimport } from 'unimport';
-import {
- EslintGlobalsPropValue,
- Entrypoint,
- WxtResolvedUnimportOptions,
- WxtDirEntry,
- WxtDirFileEntry,
-} from '~/types';
+import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '~/types';
import fs from 'fs-extra';
import { dirname, relative, resolve } from 'node:path';
import {
@@ -36,20 +29,11 @@ export async function generateTypesDir(
// type augmentation can update InlineConfig correctly. Local modules defined
// in /modules are already apart of the project, so we don't need to
// add them.
- wxt.config.modules.forEach((module) => {
+ wxt.config.userModules.forEach((module) => {
if (module.type === 'node_module' && module.configKey != null)
entries.push({ module: module.id });
});
- // Auto-imports
- if (wxt.config.imports !== false) {
- const unimport = createUnimport(wxt.config.imports);
- entries.push(await getImportsDeclarationEntry(unimport));
- if (wxt.config.imports.eslintrc.enabled) {
- entries.push(await getImportsEslintEntry(unimport, wxt.config.imports));
- }
- }
-
// browser.runtime.getURL
entries.push(await getPathsDeclarationEntry(entrypoints));
@@ -84,44 +68,6 @@ export async function generateTypesDir(
);
}
-async function getImportsDeclarationEntry(
- unimport: Unimport,
-): Promise {
- // Load project imports into unimport memory so they are output via generateTypeDeclarations
- await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
-
- return {
- path: 'types/imports.d.ts',
- text: [
- '// Generated by wxt',
- await unimport.generateTypeDeclarations(),
- '',
- ].join('\n'),
- tsReference: true,
- };
-}
-
-async function getImportsEslintEntry(
- unimport: Unimport,
- options: WxtResolvedUnimportOptions,
-): Promise {
- const globals: Record = {};
- const eslintrc = { globals };
-
- (await unimport.getImports())
- .map((i) => i.as ?? i.name)
- .filter(Boolean)
- .sort()
- .forEach((name) => {
- eslintrc.globals[name] = options.eslintrc.globalsPropValue;
- });
-
- return {
- path: options.eslintrc.filePath,
- text: JSON.stringify(eslintrc, null, 2) + '\n',
- };
-}
-
async function getPathsDeclarationEntry(
entrypoints: Entrypoint[],
): Promise {
@@ -236,20 +182,19 @@ async function getGlobalsDeclarationEntry(): Promise {
}
function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
- const text = [
- '// Generated by wxt',
- ...references.map((ref) => {
- if ('module' in ref) return `/// `;
- if (!ref.tsReference) return;
-
+ const lines = ['// Generated by wxt'];
+ references.forEach((ref) => {
+ if ('module' in ref) {
+ return lines.push(`/// `);
+ } else if (ref.tsReference) {
const absolutePath = resolve(wxt.config.wxtDir, ref.path);
const relativePath = relative(wxt.config.wxtDir, absolutePath);
- return `/// `;
- }),
- ].join('\n');
+ lines.push(`/// `);
+ }
+ });
return {
path: 'wxt.d.ts',
- text,
+ text: lines.join('\n') + '\n',
};
}
diff --git a/packages/wxt/src/core/utils/building/resolve-config.ts b/packages/wxt/src/core/utils/building/resolve-config.ts
index ab3278b4..3f7f52a4 100644
--- a/packages/wxt/src/core/utils/building/resolve-config.ts
+++ b/packages/wxt/src/core/utils/building/resolve-config.ts
@@ -22,6 +22,7 @@ import { isModuleInstalled } from '../package';
import fs from 'fs-extra';
import { normalizePath } from '../paths';
import glob from 'fast-glob';
+import { builtinModules } from '~/builtin-modules';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
@@ -127,15 +128,21 @@ export async function resolveConfig(
};
}
- const modules = await resolveWxtModules(modulesDir, mergedConfig.modules);
- const moduleOptions = modules.reduce>((map, module) => {
- if (module.configKey) {
- map[module.configKey] =
- // @ts-expect-error
- mergedConfig[module.configKey];
- }
- return map;
- }, {});
+ const userModules = await resolveWxtUserModules(
+ modulesDir,
+ mergedConfig.modules,
+ );
+ const moduleOptions = userModules.reduce>(
+ (map, module) => {
+ if (module.configKey) {
+ map[module.configKey] =
+ // @ts-expect-error
+ mergedConfig[module.configKey];
+ }
+ return map;
+ },
+ {},
+ );
return {
browser,
@@ -146,7 +153,7 @@ export async function resolveConfig(
filterEntrypoints,
env,
fsCache: createFsCache(wxtDir),
- imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
+ imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion,
@@ -175,7 +182,8 @@ export async function resolveConfig(
},
hooks: mergedConfig.hooks ?? {},
vite: mergedConfig.vite ?? (() => ({})),
- modules,
+ builtinModules,
+ userModules,
plugins: [],
...moduleOptions,
};
@@ -285,20 +293,21 @@ function resolveAnalysisConfig(
async function getUnimportOptions(
wxtDir: string,
+ srcDir: string,
logger: Logger,
config: InlineConfig,
): Promise {
if (config.imports === false) return false;
- const enabledConfig = config.imports?.eslintrc?.enabled;
- let enabled: boolean;
- switch (enabledConfig) {
+ const rawEslintEnabled = config.imports?.eslintrc?.enabled;
+ let eslintEnabled: boolean;
+ switch (rawEslintEnabled) {
case undefined:
case 'auto':
- enabled = await isModuleInstalled('eslint');
+ eslintEnabled = await isModuleInstalled('eslint');
break;
default:
- enabled = enabledConfig;
+ eslintEnabled = rawEslintEnabled;
}
const defaultOptions: WxtResolvedUnimportOptions = {
@@ -315,8 +324,11 @@ async function getUnimportOptions(
],
warn: logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
+ dirsScanOptions: {
+ cwd: srcDir,
+ },
eslintrc: {
- enabled,
+ enabled: eslintEnabled,
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
globalsPropValue: true,
},
@@ -378,7 +390,7 @@ export async function mergeBuilderConfig(
throw Error('Builder not found. Make sure vite is installed.');
}
-export async function resolveWxtModules(
+export async function resolveWxtUserModules(
modulesDir: string,
modules: string[] = [],
): Promise[]> {
diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts
index 45d18e0e..a4589e43 100644
--- a/packages/wxt/src/core/utils/testing/fake-objects.ts
+++ b/packages/wxt/src/core/utils/testing/fake-objects.ts
@@ -239,7 +239,8 @@ export const fakeResolvedConfig = fakeObjectCreator(() => {
command,
entrypointsDir: fakeDir(),
modulesDir: fakeDir(),
- modules: [],
+ builtinModules: [],
+ userModules: [],
env: { browser, command, manifestVersion, mode },
fsCache: mock(),
imports: {
diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts
index dbfd1df4..39e52b7d 100644
--- a/packages/wxt/src/core/wxt.ts
+++ b/packages/wxt/src/core/wxt.ts
@@ -5,11 +5,13 @@ import {
WxtCommand,
WxtDevServer,
WxtHooks,
+ WxtModule,
} from '~/types';
import { resolveConfig } from './utils/building';
import { createHooks } from 'hookable';
import { createWxtPackageManager } from './package-managers';
import { createViteBuilder } from './builders/vite';
+import { builtinModules } from '~/builtin-modules';
/**
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
@@ -46,18 +48,16 @@ export async function registerWxt(
};
// Initialize modules
- for (const module of config.modules) {
+ const initModule = async (module: WxtModule) => {
if (module.hooks) wxt.hooks.addHooks(module.hooks);
- if (wxt.config.imports !== false && module.imports) {
- wxt.config.imports.imports ??= [];
- wxt.config.imports.imports.push(...module.imports);
- }
await module.setup?.(
wxt,
// @ts-expect-error: Untyped configKey field
module.configKey ? config[module.configKey] : undefined,
);
- }
+ };
+ for (const builtinModule of builtinModules) await initModule(builtinModule);
+ for (const userModule of config.userModules) await initModule(userModule);
// Initialize hooks
wxt.hooks.addHooks(config.hooks);
diff --git a/packages/wxt/src/testing/wxt-vitest-plugin.ts b/packages/wxt/src/testing/wxt-vitest-plugin.ts
index 84e4775c..63281b9d 100644
--- a/packages/wxt/src/testing/wxt-vitest-plugin.ts
+++ b/packages/wxt/src/testing/wxt-vitest-plugin.ts
@@ -1,6 +1,5 @@
import type * as vite from 'vite';
import {
- unimport,
download,
tsconfigPaths,
globals,
@@ -8,6 +7,8 @@ import {
} from '../core/builders/vite/plugins';
import { resolveConfig } from '~/core/utils/building';
import { InlineConfig } from '../types';
+import { vitePlugin as unimportPlugin } from '~/builtin-modules/unimport';
+import { createUnimport } from 'unimport';
/**
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `/wxt.config.ts`
@@ -25,11 +26,18 @@ import { InlineConfig } from '../types';
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
*/
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
- return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [
- webextensionPolyfillMock(config),
- unimport(config),
- globals(config),
- download(config),
- tsconfigPaths(config),
- ]);
+ return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => {
+ const plugins = [
+ webextensionPolyfillMock(config),
+ globals(config),
+ download(config),
+ tsconfigPaths(config),
+ ];
+ if (config.imports !== false) {
+ const unimport = createUnimport(config.imports);
+ await unimport.init();
+ plugins.push(unimportPlugin(unimport));
+ }
+ return plugins;
+ });
}
diff --git a/packages/wxt/src/types/index.ts b/packages/wxt/src/types/index.ts
index 26102068..ea934288 100644
--- a/packages/wxt/src/types/index.ts
+++ b/packages/wxt/src/types/index.ts
@@ -1205,7 +1205,8 @@ export interface ResolvedConfig {
reloadCommand: string | false;
};
hooks: NestedHooks;
- modules: WxtModuleWithMetadata[];
+ builtinModules: WxtModule[];
+ userModules: WxtModuleWithMetadata[];
/**
* An array of string to import plugins from. These paths should be
* resolvable by vite, and they should `export default defineWxtPlugin(...)`.