diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index d5a612d92..18546450c 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -24,6 +24,8 @@ import { importEntrypointFile } from '../../utils/building'; import { ViteNodeServer } from 'vite-node/server'; import { ViteNodeRunner } from 'vite-node/client'; import { installSourcemapsSupport } from 'vite-node/source-map'; +import { createExtensionEnvironment } from '../../utils/environments'; +import { relative } from 'node:path'; export async function createViteBuilder( wxtConfig: ResolvedConfig, @@ -220,55 +222,104 @@ export async function createViteBuilder( }; }; + const createViteNodeImporter = async (paths: string[]) => { + const baseConfig = await getBaseConfig({ + excludeAnalysisPlugin: true, + }); + // Disable dep optimization, as recommended by vite-node's README + baseConfig.optimizeDeps ??= {}; + baseConfig.optimizeDeps.noDiscovery = true; + baseConfig.optimizeDeps.include = []; + const envConfig: vite.InlineConfig = { + plugins: paths.map((path) => + wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), + ), + }; + const config = vite.mergeConfig(baseConfig, envConfig); + const server = await vite.createServer(config); + await server.pluginContainer.buildStart({}); + const node = new ViteNodeServer( + // @ts-ignore: Some weird type error... + server, + ); + installSourcemapsSupport({ + getSourceMap: (source) => node.getSourceMap(source), + }); + const runner = new ViteNodeRunner({ + root: server.config.root, + base: server.config.base, + // when having the server and runner in a different context, + // you will need to handle the communication between them + // and pass to this function + fetchModule(id) { + return node.fetchModule(id); + }, + resolveId(id, importer) { + return node.resolveId(id, importer); + }, + }); + return { runner, server }; + }; + + const requireDefaultExport = (path: string, mod: any) => { + const relativePath = relative(wxtConfig.root, path); + if (mod?.default == null) { + const defineFn = relativePath.includes('.content') + ? 'defineContentScript' + : relativePath.includes('background') + ? 'defineBackground' + : 'defineUnlistedScript'; + + throw Error( + `${relativePath}: Default export not found, did you forget to call "export default ${defineFn}(...)"?`, + ); + } + }; + return { name: 'Vite', version: vite.version, async importEntrypoint(path) { + const env = createExtensionEnvironment(); switch (wxtConfig.entrypointLoader) { default: case 'jiti': { - return await importEntrypointFile(path); + return await env.run(() => importEntrypointFile(path)); } case 'vite-node': { - const baseConfig = await getBaseConfig({ - excludeAnalysisPlugin: true, - }); - // Disable dep optimization, as recommended by vite-node's README - baseConfig.optimizeDeps ??= {}; - baseConfig.optimizeDeps.noDiscovery = true; - baseConfig.optimizeDeps.include = []; - const envConfig: vite.InlineConfig = { - plugins: [wxtPlugins.removeEntrypointMainFunction(wxtConfig, path)], - }; - const config = vite.mergeConfig(baseConfig, envConfig); - const server = await vite.createServer(config); - await server.pluginContainer.buildStart({}); - const node = new ViteNodeServer( - // @ts-ignore: Some weird type error... - server, - ); - installSourcemapsSupport({ - getSourceMap: (source) => node.getSourceMap(source), - }); - const runner = new ViteNodeRunner({ - root: server.config.root, - base: server.config.base, - // when having the server and runner in a different context, - // you will need to handle the communication between them - // and pass to this function - fetchModule(id) { - return node.fetchModule(id); - }, - resolveId(id, importer) { - return node.resolveId(id, importer); - }, - }); - const res = await runner.executeFile(path); + const { runner, server } = await createViteNodeImporter([path]); + const res = await env.run(() => runner.executeFile(path)); await server.close(); + requireDefaultExport(path, res); return res.default; } } }, + async importEntrypoints(paths) { + const env = createExtensionEnvironment(); + switch (wxtConfig.entrypointLoader) { + default: + case 'jiti': { + return await env.run(() => + Promise.all(paths.map(importEntrypointFile)), + ); + } + case 'vite-node': { + const { runner, server } = await createViteNodeImporter(paths); + const res = await env.run(() => + Promise.all( + paths.map(async (path) => { + const mod = await runner.executeFile(path); + requireDefaultExport(path, mod); + return mod.default; + }), + ), + ); + await server.close(); + return res; + } + } + }, async build(group) { let entryConfig; if (Array.isArray(group)) entryConfig = getMultiPageConfig(group); diff --git a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts index 566a9c61b..b07f37480 100644 --- a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts @@ -36,11 +36,12 @@ describe('findEntrypoints', () => { outDir: resolve('.output'), command: 'build', }); - let importEntrypointMock: Mock; + let importEntrypointsMock: Mock; beforeEach(() => { setFakeWxt({ config }); - importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint); + importEntrypointsMock = vi.mocked(wxt.builder.importEntrypoints); + importEntrypointsMock.mockResolvedValue([]); }); it.each<[string, string, PopupEntrypoint]>([ @@ -210,13 +211,13 @@ describe('findEntrypoints', () => { matches: [''], }; globMock.mockResolvedValueOnce([path]); - importEntrypointMock.mockResolvedValue(options); + importEntrypointsMock.mockResolvedValue([options]); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]); }, ); @@ -244,17 +245,17 @@ describe('findEntrypoints', () => { ])( 'should find and load background entrypoint config from %s', async (path, expected) => { - const options: BackgroundEntrypointOptions = { + const options = { type: 'module', - }; + } satisfies BackgroundEntrypointOptions; globMock.mockResolvedValueOnce([path]); - importEntrypointMock.mockResolvedValue(options); + importEntrypointsMock.mockResolvedValue([options]); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]); }, ); @@ -339,11 +340,11 @@ describe('findEntrypoints', () => { }, builder: wxt.builder, }); - const options: BackgroundEntrypointOptions = { + const options = { type: 'module', - }; + } satisfies BackgroundEntrypointOptions; globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointMock.mockResolvedValue(options); + importEntrypointsMock.mockResolvedValue([options]); const entrypoints = await findEntrypoints(); @@ -357,11 +358,11 @@ describe('findEntrypoints', () => { }, builder: wxt.builder, }); - const options: BackgroundEntrypointOptions = { + const options = { type: 'module', - }; + } satisfies BackgroundEntrypointOptions; globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointMock.mockResolvedValue(options); + importEntrypointsMock.mockResolvedValue([options]); const entrypoints = await findEntrypoints(); @@ -410,15 +411,15 @@ describe('findEntrypoints', () => { outputDir: config.outDir, skipped: false, }; - const options: BaseEntrypointOptions = {}; + const options = {} satisfies BaseEntrypointOptions; globMock.mockResolvedValueOnce([path]); - importEntrypointMock.mockResolvedValue(options); + importEntrypointsMock.mockResolvedValue([options]); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]); }, ); @@ -703,9 +704,9 @@ describe('findEntrypoints', () => { describe('include option', () => { it("should mark the background as skipped when include doesn't contain the target browser", async () => { globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointMock.mockResolvedValue({ - include: ['not' + config.browser], - }); + importEntrypointsMock.mockResolvedValue([ + { include: ['not' + config.browser] }, + ]); const entrypoints = await findEntrypoints(); @@ -719,9 +720,9 @@ describe('findEntrypoints', () => { it("should mark content scripts as skipped when include doesn't contain the target browser", async () => { globMock.mockResolvedValueOnce(['example.content.ts']); - importEntrypointMock.mockResolvedValue({ - include: ['not' + config.browser], - }); + importEntrypointsMock.mockResolvedValue([ + { include: ['not' + config.browser] }, + ]); const entrypoints = await findEntrypoints(); @@ -803,9 +804,7 @@ describe('findEntrypoints', () => { describe('exclude option', () => { it('should mark the background as skipped when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointMock.mockResolvedValue({ - exclude: [config.browser], - }); + importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]); const entrypoints = await findEntrypoints(); @@ -819,9 +818,7 @@ describe('findEntrypoints', () => { it('should mark content scripts as skipped when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['example.content.ts']); - importEntrypointMock.mockResolvedValue({ - exclude: [config.browser], - }); + importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]); const entrypoints = await findEntrypoints(); @@ -914,7 +911,7 @@ describe('findEntrypoints', () => { builder: wxt.builder, }); - importEntrypointMock.mockResolvedValue({}); + importEntrypointsMock.mockResolvedValue([{}]); const entrypoints = await findEntrypoints(); diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 0f71d07f8..92c6fd404 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -1,19 +1,14 @@ import { relative, resolve } from 'path'; import { BackgroundEntrypoint, - BackgroundDefinition, - BaseEntrypointOptions, - ContentScriptDefinition, ContentScriptEntrypoint, Entrypoint, GenericEntrypoint, OptionsEntrypoint, PopupEntrypoint, - UnlistedScriptDefinition, - PopupEntrypointOptions, - OptionsEntrypointOptions, SidepanelEntrypoint, - SidepanelEntrypointOptions, + MainWorldContentScriptEntrypointOptions, + IsolatedWorldContentScriptEntrypointOptions, } from '../../../types'; import fs from 'fs-extra'; import { minimatch } from 'minimatch'; @@ -22,13 +17,15 @@ import JSON5 from 'json5'; import glob from 'fast-glob'; import { getEntrypointName, + isHtmlEntrypoint, + isJsEntrypoint, resolvePerBrowserOptions, } from '../../utils/entrypoints'; import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants'; import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths'; import pc from 'picocolors'; import { wxt } from '../../wxt'; -import { createExtensionEnvironment } from '../environments'; +import { camelCase } from 'scule'; /** * Return entrypoints and their configuration by looking through the project's files. @@ -76,59 +73,61 @@ export async function findEntrypoints(): Promise { // Import entrypoints to get their config let hasBackground = false; - const env = createExtensionEnvironment(); - const entrypointsWithoutSkipped: Entrypoint[] = await env.run(() => - Promise.all( - entrypointInfos.map(async (info): Promise => { - const { type } = info; - switch (type) { - case 'popup': - return await getPopupEntrypoint(info); - case 'sidepanel': - return await getSidepanelEntrypoint(info); - case 'options': - return await getOptionsEntrypoint(info); - case 'background': - hasBackground = true; - return await getBackgroundEntrypoint(info); - case 'content-script': - return await getContentScriptEntrypoint(info); - case 'unlisted-page': - return await getUnlistedPageEntrypoint(info); - case 'unlisted-script': - return await getUnlistedScriptEntrypoint(info); - case 'content-script-style': - return { - ...info, - type, - outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), - options: { - include: undefined, - exclude: undefined, - }, - }; - default: - return { - ...info, - type, - outputDir: wxt.config.outDir, - options: { - include: undefined, - exclude: undefined, - }, - }; - } - }), - ), + const entrypointOptions = await importEntrypoints(entrypointInfos); + const entrypointsWithoutSkipped: Entrypoint[] = await Promise.all( + entrypointInfos.map(async (info): Promise => { + const { type } = info; + const options = entrypointOptions[info.inputPath] ?? {}; + switch (type) { + case 'popup': + return await getPopupEntrypoint(info, options); + case 'sidepanel': + return await getSidepanelEntrypoint(info, options); + case 'options': + return await getOptionsEntrypoint(info, options); + case 'background': + hasBackground = true; + return await getBackgroundEntrypoint(info, options); + case 'content-script': + return await getContentScriptEntrypoint(info, options); + case 'unlisted-page': + return await getUnlistedPageEntrypoint(info, options); + case 'unlisted-script': + return await getUnlistedScriptEntrypoint(info, options); + case 'content-script-style': + return { + ...info, + type, + outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), + options: { + include: options.include, + exclude: options.exclude, + }, + }; + default: + return { + ...info, + type, + outputDir: wxt.config.outDir, + options: { + include: options.include, + exclude: options.exclude, + }, + }; + } + }), ); if (wxt.config.command === 'serve' && !hasBackground) { entrypointsWithoutSkipped.push( - await getBackgroundEntrypoint({ - inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID, - name: 'background', - type: 'background', - }), + await getBackgroundEntrypoint( + { + inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID, + name: 'background', + type: 'background', + }, + {}, + ), ); } @@ -163,6 +162,61 @@ interface EntrypointInfo { type: Entrypoint['type']; } +/** Returns a map of input paths to the file's options. */ +async function importEntrypoints(infos: EntrypointInfo[]) { + const resMap: Record | undefined> = {}; + + const htmlInfos = infos.filter((info) => isHtmlEntrypoint(info)); + const jsInfos = infos.filter((info) => isJsEntrypoint(info)); + + await Promise.all([ + // HTML + ...htmlInfos.map(async (info) => { + const res = await importHtmlEntrypoint(info); + resMap[info.inputPath] = res; + }), + // JS + (async () => { + const res = await wxt.builder.importEntrypoints( + jsInfos.map((info) => info.inputPath), + ); + res.forEach((res, i) => { + resMap[jsInfos[i].inputPath] = res; + }); + })(), + // CSS - never has options + ]); + + return resMap; +} + +/** Extract `manifest.` options from meta tags, converting snake_case keys to camelCase */ +async function importHtmlEntrypoint( + info: EntrypointInfo, +): Promise> { + const content = await fs.readFile(info.inputPath, 'utf-8'); + const { document } = parseHTML(content); + + const metaTags = document.querySelectorAll('meta'); + const res: Record = { + title: document.querySelector('title')?.textContent || undefined, + }; + + metaTags.forEach((tag) => { + const name = tag.name; + if (!name.startsWith('manifest.')) return; + + const key = camelCase(name.slice(9)); + try { + res[key] = JSON5.parse(tag.content); + } catch { + res[key] = tag.content; + } + }); + + return res; +} + function preventDuplicateEntrypointNames(files: EntrypointInfo[]) { const namesToPaths = files.reduce>( (map, { name, inputPath }) => { @@ -200,32 +254,26 @@ function preventNoEntrypoints(files: EntrypointInfo[]) { async function getPopupEntrypoint( info: EntrypointInfo, + options: Record, ): Promise { - const options = await getHtmlEntrypointOptions( - info, + const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions( { - browserStyle: 'browser_style', - exclude: 'exclude', - include: 'include', - defaultIcon: 'default_icon', - defaultTitle: 'default_title', - mv2Key: 'type', - }, - { - defaultTitle: (document) => - document.querySelector('title')?.textContent || undefined, - }, - { - defaultTitle: (content) => content, - mv2Key: (content) => - content === 'page_action' ? 'page_action' : 'browser_action', + browserStyle: options.browserStyle, + exclude: options.exclude, + include: options.include, + defaultIcon: options.defaultIcon, + defaultTitle: options.title, + mv2Key: options.type, }, + wxt.config.browser, ); + if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action') + stictOptions.mv2Key = 'browser_action'; return { type: 'popup', name: 'popup', - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: stictOptions, inputPath: info.inputPath, outputDir: wxt.config.outDir, }; @@ -233,21 +281,21 @@ async function getPopupEntrypoint( async function getOptionsEntrypoint( info: EntrypointInfo, + options: Record, ): Promise { - const options = await getHtmlEntrypointOptions( - info, - { - browserStyle: 'browser_style', - chromeStyle: 'chrome_style', - exclude: 'exclude', - include: 'include', - openInTab: 'open_in_tab', - }, - ); return { type: 'options', name: 'options', - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: resolvePerBrowserOptions( + { + browserStyle: options.browserStyle, + chromeStyle: options.chromeStyle, + exclude: options.exclude, + include: options.include, + openInTab: options.openInTab, + }, + wxt.config.browser, + ), inputPath: info.inputPath, outputDir: wxt.config.outDir, }; @@ -255,61 +303,56 @@ async function getOptionsEntrypoint( async function getUnlistedPageEntrypoint( info: EntrypointInfo, + options: Record, ): Promise { - const options = await getHtmlEntrypointOptions(info, { - exclude: 'exclude', - include: 'include', - }); - return { type: 'unlisted-page', name: info.name, inputPath: info.inputPath, outputDir: wxt.config.outDir, - options, + options: { + include: options.include, + exclude: options.exclude, + }, }; } -async function getUnlistedScriptEntrypoint({ - inputPath, - name, -}: EntrypointInfo): Promise { - const defaultExport = - await wxt.builder.importEntrypoint(inputPath); - if (defaultExport == null) { - throw Error( - `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`, - ); - } - const { main: _, ...options } = defaultExport; +async function getUnlistedScriptEntrypoint( + { inputPath, name }: EntrypointInfo, + options: Record, +): Promise { return { type: 'unlisted-script', name, inputPath, outputDir: wxt.config.outDir, - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: resolvePerBrowserOptions( + { + include: options.include, + exclude: options.exclude, + }, + wxt.config.browser, + ), }; } -async function getBackgroundEntrypoint({ - inputPath, - name, -}: EntrypointInfo): Promise { - let options: Omit = {}; - if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) { - const defaultExport = - await wxt.builder.importEntrypoint(inputPath); - if (defaultExport == null) { - throw Error( - `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`, - ); - } - const { main: _, ...moduleOptions } = defaultExport; - options = moduleOptions; - } +async function getBackgroundEntrypoint( + { inputPath, name }: EntrypointInfo, + options: Record, +): Promise { + const strictOptions: BackgroundEntrypoint['options'] = + resolvePerBrowserOptions( + { + include: options.include, + exclude: options.exclude, + persistent: options.persistent, + type: options.type, + }, + wxt.config.browser, + ); if (wxt.config.manifestVersion !== 3) { - delete options.type; + delete strictOptions.type; } return { @@ -317,112 +360,51 @@ async function getBackgroundEntrypoint({ name, inputPath, outputDir: wxt.config.outDir, - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: strictOptions, }; } -async function getContentScriptEntrypoint({ - inputPath, - name, -}: EntrypointInfo): Promise { - const defaultExport = - await wxt.builder.importEntrypoint(inputPath); - if (defaultExport == null) { - throw Error( - `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`, - ); - } - - const { main: _, ...options } = defaultExport; - if (options == null) { - throw Error( - `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`, - ); - } +async function getContentScriptEntrypoint( + { inputPath, name }: EntrypointInfo, + options: Record, +): Promise { return { type: 'content-script', name, inputPath, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: resolvePerBrowserOptions( + options as + | MainWorldContentScriptEntrypointOptions + | IsolatedWorldContentScriptEntrypointOptions, + wxt.config.browser, + ), }; } async function getSidepanelEntrypoint( info: EntrypointInfo, + options: Record, ): Promise { - const options = await getHtmlEntrypointOptions( - info, - { - browserStyle: 'browser_style', - exclude: 'exclude', - include: 'include', - defaultIcon: 'default_icon', - defaultTitle: 'default_title', - openAtInstall: 'open_at_install', - }, - { - defaultTitle: (document) => - document.querySelector('title')?.textContent || undefined, - }, - { - defaultTitle: (content) => content, - }, - ); - return { type: 'sidepanel', name: info.name, - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: resolvePerBrowserOptions( + { + browserStyle: options.browserStyle, + exclude: options.exclude, + include: options.include, + defaultIcon: options.defaultIcon, + defaultTitle: options.title, + openAtInstall: options.openAtInstall, + }, + wxt.config.browser, + ), inputPath: info.inputPath, outputDir: wxt.config.outDir, }; } -/** - * Parse the HTML tags to extract options from them. - */ -async function getHtmlEntrypointOptions( - info: EntrypointInfo, - keyMap: Record, - queries?: Partial<{ - [key in keyof T]: ( - document: Document, - manifestKey: string, - ) => string | undefined; - }>, - parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>, -): Promise { - const content = await fs.readFile(info.inputPath, 'utf-8'); - const { document } = parseHTML(content); - - const options = {} as T; - - const defaultQuery = (manifestKey: string) => - document - .querySelector(`meta[name='manifest.${manifestKey}']`) - ?.getAttribute('content'); - - Object.entries(keyMap).forEach(([_key, manifestKey]) => { - const key = _key as keyof T; - const content = queries?.[key] - ? queries[key]!(document, manifestKey) - : defaultQuery(manifestKey); - if (content) { - try { - options[key] = (parsers?.[key] ?? JSON5.parse)(content); - } catch (err) { - wxt.logger.fatal( - `Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content})`, - err, - ); - } - } - }); - - return options; -} - function isEntrypointSkipped(entry: Omit): boolean { if (wxt.config.filterEntrypoints != null) { return !wxt.config.filterEntrypoints.has(entry.name); diff --git a/packages/wxt/src/core/utils/entrypoints.ts b/packages/wxt/src/core/utils/entrypoints.ts index 96e2bd155..8ada4835c 100644 --- a/packages/wxt/src/core/utils/entrypoints.ts +++ b/packages/wxt/src/core/utils/entrypoints.ts @@ -4,7 +4,7 @@ import { ResolvedPerBrowserOptions, TargetBrowser, } from '../../types'; -import path, { relative, resolve } from 'node:path'; +import path, { relative, resolve, extname } from 'node:path'; import { normalizePath } from './paths'; export function getEntrypointName( @@ -76,6 +76,21 @@ export function resolvePerBrowserOptions< * * Naively just checking the file extension of the input path. */ -export function isHtmlEntrypoint(entrypoint: Entrypoint): boolean { - return entrypoint.inputPath.endsWith('.html'); +export function isHtmlEntrypoint( + entrypoint: Pick, +): boolean { + const ext = extname(entrypoint.inputPath); + return ['.html'].includes(ext); +} + +/** + * Returns true when the entrypoint is a JS entrypoint. + * + * Naively just checking the file extension of the input path. + */ +export function isJsEntrypoint( + entrypoint: Pick, +): boolean { + const ext = extname(entrypoint.inputPath); + return ['.js', '.jsx', '.ts', '.tsx'].includes(ext); } diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index e39c89e56..beacfc267 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1035,9 +1035,13 @@ export interface WxtBuilder { */ version: string; /** - * Import the entrypoint file, returning the default export containing the options. + * Import a JS entrypoint file, returning the default export containing the options. */ importEntrypoint(path: string): Promise; + /** + * Import a list of JS entrypoint files, returning their options. + */ + importEntrypoints(paths: string[]): Promise[]>; /** * Build a single entrypoint group. This is effectively one of the multiple "steps" during the * build process.