From 96c43251ceb22995d2617ec3b152448a833ae91a Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 25 Nov 2024 15:05:17 -0600 Subject: [PATCH 1/9] fix: Use a single Vite server when importing entrypoints during build to reduce number of event listeners --- .../wxt-demo/src/entrypoints/_four.content.ts | 4 ++++ .../wxt-demo/src/entrypoints/_one.contente.ts | 4 ++++ .../src/entrypoints/_three.content.ts | 4 ++++ .../wxt-demo/src/entrypoints/_two.contente.ts | 4 ++++ .../core/utils/building/find-entrypoints.ts | 20 ++++++++++++++----- 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 packages/wxt-demo/src/entrypoints/_four.content.ts create mode 100644 packages/wxt-demo/src/entrypoints/_one.contente.ts create mode 100644 packages/wxt-demo/src/entrypoints/_three.content.ts create mode 100644 packages/wxt-demo/src/entrypoints/_two.contente.ts diff --git a/packages/wxt-demo/src/entrypoints/_four.content.ts b/packages/wxt-demo/src/entrypoints/_four.content.ts new file mode 100644 index 000000000..c3a5f13c2 --- /dev/null +++ b/packages/wxt-demo/src/entrypoints/_four.content.ts @@ -0,0 +1,4 @@ +export default defineContentScript({ + matches: [], + main() {}, +}); diff --git a/packages/wxt-demo/src/entrypoints/_one.contente.ts b/packages/wxt-demo/src/entrypoints/_one.contente.ts new file mode 100644 index 000000000..c3a5f13c2 --- /dev/null +++ b/packages/wxt-demo/src/entrypoints/_one.contente.ts @@ -0,0 +1,4 @@ +export default defineContentScript({ + matches: [], + main() {}, +}); diff --git a/packages/wxt-demo/src/entrypoints/_three.content.ts b/packages/wxt-demo/src/entrypoints/_three.content.ts new file mode 100644 index 000000000..c3a5f13c2 --- /dev/null +++ b/packages/wxt-demo/src/entrypoints/_three.content.ts @@ -0,0 +1,4 @@ +export default defineContentScript({ + matches: [], + main() {}, +}); diff --git a/packages/wxt-demo/src/entrypoints/_two.contente.ts b/packages/wxt-demo/src/entrypoints/_two.contente.ts new file mode 100644 index 000000000..c3a5f13c2 --- /dev/null +++ b/packages/wxt-demo/src/entrypoints/_two.contente.ts @@ -0,0 +1,4 @@ +export default defineContentScript({ + matches: [], + main() {}, +}); diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 12ceb0991..cca5f481f 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -80,10 +80,20 @@ export async function findEntrypoints(): Promise { // Import entrypoints to get their config let hasBackground = false; const env = createExtensionEnvironment(); - const entrypoints: Entrypoint[] = await env.run(() => - Promise.all( - entrypointInfos.map(async (info): Promise => { + const entrypoints: Entrypoint[] = await env.run(() => { + const res = wxt.builder.importEntrypoints( + entrypointInfos + .filter( + (info) => + info.type === 'content-script' || info.type === 'unlisted-script', + ) + .map((info) => info.inputPath), + ); + + return Promise.all( + entrypointInfos.map(async (info, i): Promise => { const { type } = info; + const defaultExport = res[i]; switch (type) { case 'popup': return await getPopupEntrypoint(info); @@ -122,8 +132,8 @@ export async function findEntrypoints(): Promise { }; } }), - ), - ); + ); + }); if (wxt.config.command === 'serve' && !hasBackground) { entrypoints.push( From 5bffbc1c6233c568fd441717a029d5a79ca516ae Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 10:04:35 -0600 Subject: [PATCH 2/9] Implement shared logic --- .../core/utils/building/find-entrypoints.ts | 364 +++++++++--------- packages/wxt/src/core/utils/entrypoints.ts | 21 +- packages/wxt/src/types.ts | 10 +- 3 files changed, 200 insertions(+), 195 deletions(-) diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index cca5f481f..5f12bd000 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,6 +17,8 @@ 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'; @@ -29,6 +26,7 @@ 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. @@ -79,45 +77,41 @@ export async function findEntrypoints(): Promise { // Import entrypoints to get their config let hasBackground = false; - const env = createExtensionEnvironment(); - const entrypoints: Entrypoint[] = await env.run(() => { - const res = wxt.builder.importEntrypoints( - entrypointInfos - .filter( - (info) => - info.type === 'content-script' || info.type === 'unlisted-script', - ) - .map((info) => info.inputPath), - ); - return Promise.all( - entrypointInfos.map(async (info, i): Promise => { + // 1. Import all files + const entrypointOptions = await importEntrypoints(entrypointInfos); + + // 2. Use options returned to construct entrypoints list + const env = createExtensionEnvironment(); + const entrypoints: Entrypoint[] = await env.run(() => + Promise.all( + entrypointInfos.map(async (info): Promise => { const { type } = info; - const defaultExport = res[i]; + const options = entrypointOptions[info.inputPath] ?? {}; switch (type) { case 'popup': - return await getPopupEntrypoint(info); + return await getPopupEntrypoint(info, options); case 'sidepanel': - return await getSidepanelEntrypoint(info); + return await getSidepanelEntrypoint(info, options); case 'options': - return await getOptionsEntrypoint(info); + return await getOptionsEntrypoint(info, options); case 'background': hasBackground = true; - return await getBackgroundEntrypoint(info); + return await getBackgroundEntrypoint(info, options); case 'content-script': - return await getContentScriptEntrypoint(info); + return await getContentScriptEntrypoint(info, options); case 'unlisted-page': - return await getUnlistedPageEntrypoint(info); + return await getUnlistedPageEntrypoint(info, options); case 'unlisted-script': - return await getUnlistedScriptEntrypoint(info); + return await getUnlistedScriptEntrypoint(info, options); case 'content-script-style': return { ...info, type, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), options: { - include: undefined, - exclude: undefined, + include: (options as any).include, + exclude: (options as any).exclude, }, }; default: @@ -126,23 +120,26 @@ export async function findEntrypoints(): Promise { type, outputDir: wxt.config.outDir, options: { - include: undefined, - exclude: undefined, + include: (options as any).include, + exclude: (options as any).exclude, }, }; } }), - ); - }); + ), + ); if (wxt.config.command === 'serve' && !hasBackground) { entrypoints.push( - await getBackgroundEntrypoint({ - inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID, - name: 'background', - type: 'background', - skipped: false, - }), + await getBackgroundEntrypoint( + { + inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID, + name: 'background', + type: 'background', + skipped: false, + }, + {}, + ), ); } @@ -197,6 +194,59 @@ interface EntrypointInfo { skipped: boolean; } +/** 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 + wxt.builder + .importJsEntrypoints(jsInfos.map((info) => info.inputPath)) + .then((res) => { + 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.title, + }; + + // Non-json5 keys + const stringKeys = ['defaultTitle']; + + metaTags.forEach((tag) => { + const name = tag.name; + if (!name.startsWith('manifest.')) return; + + const key = camelCase(name.slice(9)); + res[key] = stringKeys.includes(key) ? content : JSON5.parse(tag.content); + }); + + return res; +} + function preventDuplicateEntrypointNames(files: Entrypoint[]) { const namesToPaths = files.reduce>( (map, { name, inputPath }) => { @@ -234,32 +284,26 @@ function preventNoEntrypoints(files: Entrypoint[]) { async function getPopupEntrypoint( info: EntrypointInfo, + options: Record, ): Promise { - const options = await getHtmlEntrypointOptions( - info, - { - browserStyle: 'browser_style', - exclude: 'exclude', - include: 'include', - defaultIcon: 'default_icon', - defaultTitle: 'default_title', - mv2Key: 'type', - }, + const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions( { - 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 !== '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, skipped: info.skipped, @@ -268,21 +312,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, skipped: info.skipped, @@ -291,65 +335,65 @@ 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, + }, skipped: info.skipped, }; } -async function getUnlistedScriptEntrypoint({ - inputPath, - name, - skipped, -}: 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, skipped }: EntrypointInfo, + options: Record, +): Promise { + // TODO: Move into `builder` + // if (defaultExport == null) { + // throw Error( + // `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`, + // ); + // } + // const { main: _, ...options } = defaultExport; 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, + ), skipped, }; } -async function getBackgroundEntrypoint({ - inputPath, - name, - skipped, -}: 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, skipped }: 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 { @@ -357,116 +401,54 @@ async function getBackgroundEntrypoint({ name, inputPath, outputDir: wxt.config.outDir, - options: resolvePerBrowserOptions(options, wxt.config.browser), + options: strictOptions, skipped, }; } -async function getContentScriptEntrypoint({ - inputPath, - name, - skipped, -}: 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, skipped }: 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, + ), skipped, }; } 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, skipped: info.skipped, }; } -/** - * 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; -} - const PATH_GLOB_TO_TYPE_MAP: Record = { 'sandbox.html': 'sandbox', 'sandbox/index.html': 'sandbox', diff --git a/packages/wxt/src/core/utils/entrypoints.ts b/packages/wxt/src/core/utils/entrypoints.ts index 96e2bd155..dc30dbd21 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 0582f81e1..657f3f7b8 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1027,9 +1027,17 @@ export interface WxtBuilder { */ version: string; /** - * Import the entrypoint file, returning the default export containing the options. + * @deprecated Use `importJsEntrypoint` instead. Same function, better name. */ importEntrypoint(path: string): Promise; + /** + * Import an JS entrypoint file, returning it's options. + */ + importJsEntrypoint(path: string): Promise; + /** + * Import a list of JS entrypoint files, returning their options. + */ + importJsEntrypoints(paths: string[]): Promise[]>; /** * Build a single entrypoint group. This is effectively one of the multiple "steps" during the * build process. From c614bc6e88b8773b3b7a0835c9329860e013694a Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 10:15:27 -0600 Subject: [PATCH 3/9] implement vite builder --- packages/wxt/src/core/builders/vite/index.ts | 103 ++++++++++++------ .../core/utils/building/find-entrypoints.ts | 2 +- packages/wxt/src/types.ts | 8 +- 3 files changed, 71 insertions(+), 42 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index d5a612d92..2b4937ba8 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -24,6 +24,7 @@ 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'; export async function createViteBuilder( wxtConfig: ResolvedConfig, @@ -220,55 +221,87 @@ 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 }; + }; + 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(); 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); + 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/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 5f12bd000..174fd4a85 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -209,7 +209,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) { }), // JS wxt.builder - .importJsEntrypoints(jsInfos.map((info) => info.inputPath)) + .importEntrypoints(jsInfos.map((info) => info.inputPath)) .then((res) => { res.forEach((res, i) => { resMap[jsInfos[i].inputPath] = res; diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 657f3f7b8..a586ac3ab 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1027,17 +1027,13 @@ export interface WxtBuilder { */ version: string; /** - * @deprecated Use `importJsEntrypoint` instead. Same function, better name. + * TODO */ importEntrypoint(path: string): Promise; - /** - * Import an JS entrypoint file, returning it's options. - */ - importJsEntrypoint(path: string): Promise; /** * Import a list of JS entrypoint files, returning their options. */ - importJsEntrypoints(paths: string[]): Promise[]>; + importEntrypoints(paths: string[]): Promise[]>; /** * Build a single entrypoint group. This is effectively one of the multiple "steps" during the * build process. From 055d569512644b125e3cea7dccb8f95503edba6c Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 10:24:46 -0600 Subject: [PATCH 4/9] fix filtering --- packages/wxt/src/core/utils/entrypoints.ts | 4 ++-- packages/wxt/src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wxt/src/core/utils/entrypoints.ts b/packages/wxt/src/core/utils/entrypoints.ts index dc30dbd21..8ada4835c 100644 --- a/packages/wxt/src/core/utils/entrypoints.ts +++ b/packages/wxt/src/core/utils/entrypoints.ts @@ -80,7 +80,7 @@ export function isHtmlEntrypoint( entrypoint: Pick, ): boolean { const ext = extname(entrypoint.inputPath); - return ['html'].includes(ext); + return ['.html'].includes(ext); } /** @@ -92,5 +92,5 @@ export function isJsEntrypoint( entrypoint: Pick, ): boolean { const ext = extname(entrypoint.inputPath); - return ['js', 'jsx', 'ts', 'tsx'].includes(ext); + return ['.js', '.jsx', '.ts', '.tsx'].includes(ext); } diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index a586ac3ab..2742f275a 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1027,7 +1027,7 @@ export interface WxtBuilder { */ version: string; /** - * TODO + * Import a JS entrypoint file, returning the default export containing the options. */ importEntrypoint(path: string): Promise; /** From 7c27f81875d8481ac85a1de94fb7d3bce56d4b45 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 10:27:14 -0600 Subject: [PATCH 5/9] Cleanup --- .../core/utils/building/find-entrypoints.ts | 92 +++++++++---------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 174fd4a85..bacf4cee5 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -25,7 +25,6 @@ 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'; /** @@ -77,56 +76,49 @@ export async function findEntrypoints(): Promise { // Import entrypoints to get their config let hasBackground = false; - - // 1. Import all files const entrypointOptions = await importEntrypoints(entrypointInfos); - - // 2. Use options returned to construct entrypoints list - const env = createExtensionEnvironment(); - const entrypoints: Entrypoint[] = await env.run(() => - 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 as any).include, - exclude: (options as any).exclude, - }, - }; - default: - return { - ...info, - type, - outputDir: wxt.config.outDir, - options: { - include: (options as any).include, - exclude: (options as any).exclude, - }, - }; - } - }), - ), + const entrypoints: 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 as any).include, + exclude: (options as any).exclude, + }, + }; + default: + return { + ...info, + type, + outputDir: wxt.config.outDir, + options: { + include: (options as any).include, + exclude: (options as any).exclude, + }, + }; + } + }), ); if (wxt.config.command === 'serve' && !hasBackground) { From f8d77d0309ba8a7cfb2bd8e266bf666b5fa3442c Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 10:56:50 -0600 Subject: [PATCH 6/9] Cleanup --- .../src/core/utils/building/find-entrypoints.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index bacf4cee5..554282bfc 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -103,8 +103,8 @@ export async function findEntrypoints(): Promise { type, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), options: { - include: (options as any).include, - exclude: (options as any).exclude, + include: options.include, + exclude: options.exclude, }, }; default: @@ -113,8 +113,8 @@ export async function findEntrypoints(): Promise { type, outputDir: wxt.config.outDir, options: { - include: (options as any).include, - exclude: (options as any).exclude, + include: options.include, + exclude: options.exclude, }, }; } @@ -225,15 +225,16 @@ async function importHtmlEntrypoint( title: document.title, }; - // Non-json5 keys - const stringKeys = ['defaultTitle']; - metaTags.forEach((tag) => { const name = tag.name; if (!name.startsWith('manifest.')) return; const key = camelCase(name.slice(9)); - res[key] = stringKeys.includes(key) ? content : JSON5.parse(tag.content); + try { + res[key] = JSON5.parse(tag.content); + } catch { + res[key] = tag.content; + } }); return res; From 3dcd92a3f570a2be7f9e892603942a4c4192ff00 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 11:07:22 -0600 Subject: [PATCH 7/9] add back error --- packages/wxt/src/core/builders/vite/index.ts | 18 ++++++++++++++++++ .../core/utils/building/find-entrypoints.ts | 7 ------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 2b4937ba8..18546450c 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -25,6 +25,7 @@ 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, @@ -260,6 +261,21 @@ export async function createViteBuilder( 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, @@ -274,6 +290,7 @@ export async function createViteBuilder( const { runner, server } = await createViteNodeImporter([path]); const res = await env.run(() => runner.executeFile(path)); await server.close(); + requireDefaultExport(path, res); return res.default; } } @@ -293,6 +310,7 @@ export async function createViteBuilder( Promise.all( paths.map(async (path) => { const mod = await runner.executeFile(path); + requireDefaultExport(path, mod); return mod.default; }), ), diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 554282bfc..c5deda1a3 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -347,13 +347,6 @@ async function getUnlistedScriptEntrypoint( { inputPath, name, skipped }: EntrypointInfo, options: Record, ): Promise { - // TODO: Move into `builder` - // if (defaultExport == null) { - // throw Error( - // `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`, - // ); - // } - // const { main: _, ...options } = defaultExport; return { type: 'unlisted-script', name, From a621ba97e7e38b2fcbc3a1eab16e9d3bccf8c910 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 11:09:13 -0600 Subject: [PATCH 8/9] Cleanup: --- packages/wxt-demo/src/entrypoints/_four.content.ts | 4 ---- packages/wxt-demo/src/entrypoints/_one.contente.ts | 4 ---- packages/wxt-demo/src/entrypoints/_three.content.ts | 4 ---- packages/wxt-demo/src/entrypoints/_two.contente.ts | 4 ---- 4 files changed, 16 deletions(-) delete mode 100644 packages/wxt-demo/src/entrypoints/_four.content.ts delete mode 100644 packages/wxt-demo/src/entrypoints/_one.contente.ts delete mode 100644 packages/wxt-demo/src/entrypoints/_three.content.ts delete mode 100644 packages/wxt-demo/src/entrypoints/_two.contente.ts diff --git a/packages/wxt-demo/src/entrypoints/_four.content.ts b/packages/wxt-demo/src/entrypoints/_four.content.ts deleted file mode 100644 index c3a5f13c2..000000000 --- a/packages/wxt-demo/src/entrypoints/_four.content.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineContentScript({ - matches: [], - main() {}, -}); diff --git a/packages/wxt-demo/src/entrypoints/_one.contente.ts b/packages/wxt-demo/src/entrypoints/_one.contente.ts deleted file mode 100644 index c3a5f13c2..000000000 --- a/packages/wxt-demo/src/entrypoints/_one.contente.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineContentScript({ - matches: [], - main() {}, -}); diff --git a/packages/wxt-demo/src/entrypoints/_three.content.ts b/packages/wxt-demo/src/entrypoints/_three.content.ts deleted file mode 100644 index c3a5f13c2..000000000 --- a/packages/wxt-demo/src/entrypoints/_three.content.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineContentScript({ - matches: [], - main() {}, -}); diff --git a/packages/wxt-demo/src/entrypoints/_two.contente.ts b/packages/wxt-demo/src/entrypoints/_two.contente.ts deleted file mode 100644 index c3a5f13c2..000000000 --- a/packages/wxt-demo/src/entrypoints/_two.contente.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineContentScript({ - matches: [], - main() {}, -}); From 129cf5e69bd2eb9adc20beb844152b55fb3171ac Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 8 Dec 2024 11:38:17 -0600 Subject: [PATCH 9/9] fix tests --- .../__tests__/find-entrypoints.test.ts | 57 +++++++++---------- .../core/utils/building/find-entrypoints.ts | 19 ++++--- 2 files changed, 37 insertions(+), 39 deletions(-) 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 ced06d67b..92c6fd404 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -176,13 +176,14 @@ async function importEntrypoints(infos: EntrypointInfo[]) { resMap[info.inputPath] = res; }), // JS - wxt.builder - .importEntrypoints(jsInfos.map((info) => info.inputPath)) - .then((res) => { - res.forEach((res, i) => { - resMap[jsInfos[i].inputPath] = res; - }); - }), + (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 ]); @@ -198,7 +199,7 @@ async function importHtmlEntrypoint( const metaTags = document.querySelectorAll('meta'); const res: Record = { - title: document.title, + title: document.querySelector('title')?.textContent || undefined, }; metaTags.forEach((tag) => { @@ -266,7 +267,7 @@ async function getPopupEntrypoint( }, wxt.config.browser, ); - if (stictOptions.mv2Key !== 'page_action') + if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action') stictOptions.mv2Key = 'browser_action'; return {