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(...)`.