diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index ba332da4..e1571334 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ open: true, }, experimental: { - viteRuntime: true, + entrypointImporter: 'vite-node', }, runner: { startUrls: ['https://duckduckgo.com'], diff --git a/packages/wxt/e2e/tests/manifest-content.test.ts b/packages/wxt/e2e/tests/manifest-content.test.ts index fff27e5b..494533f1 100644 --- a/packages/wxt/e2e/tests/manifest-content.test.ts +++ b/packages/wxt/e2e/tests/manifest-content.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { TestProject } from '../utils'; -describe.each([true, false])( +describe.each(['jiti', 'vite-runtime', 'vite-node'] as const)( 'Manifest Content (Vite runtime? %s)', - (viteRuntime) => { + (entrypointImporter) => { it.each([ { browser: undefined, outDir: 'chrome-mv3', expected: undefined }, { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined }, @@ -24,7 +24,7 @@ describe.each([true, false])( main: () => {}, })`, ); - await project.build({ browser, experimental: { viteRuntime } }); + await project.build({ browser, experimental: { entrypointImporter } }); const safariManifest = await project.getOutputManifest( `.output/${outDir}/manifest.json`, diff --git a/packages/wxt/package.json b/packages/wxt/package.json index de6eec38..430b2b35 100644 --- a/packages/wxt/package.json +++ b/packages/wxt/package.json @@ -138,6 +138,7 @@ "publish-browser-extension": "^2.1.3", "unimport": "^3.7.2", "vite": "^5.3.0", + "vite-node": "^1.6.0", "web-ext-run": "^0.2.0", "webextension-polyfill": "^0.10.0" }, diff --git a/packages/wxt/src/core/builders/vite/__tests__/index.test.ts b/packages/wxt/src/core/builders/vite/__tests__/index.test.ts deleted file mode 100644 index 3d476f8b..00000000 --- a/packages/wxt/src/core/builders/vite/__tests__/index.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { createViteBuilder } from '../index'; -import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects'; -import { createHooks } from 'hookable'; - -describe('Vite Builder', () => { - describe('importEntrypoint', () => { - it('should import entrypoints, removing runtime values (like the main function)', async () => { - const { - default: { main: _, ...expected }, - } = await import('./fixtures/module'); - const builder = await createViteBuilder( - fakeResolvedConfig({ root: __dirname }), - createHooks(), - ); - const actual = await builder.importEntrypoint<{ default: any }>( - './fixtures/module.ts', - ); - expect(actual).toEqual(expected); - }); - }); -}); diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index b466295b..800b750e 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -20,6 +20,10 @@ import { import { Hookable } from 'hookable'; import { toArray } from '~/core/utils/arrays'; import { safeVarName } from '~/core/utils/strings'; +import { importEntrypointFile } from '~/core/utils/building'; +import { ViteNodeServer } from 'vite-node/server'; +import { ViteNodeRunner } from 'vite-node/client'; +import { installSourcemapsSupport } from 'vite-node/source-map'; export async function createViteBuilder( wxtConfig: ResolvedConfig, @@ -204,21 +208,65 @@ export async function createViteBuilder( return { name: 'Vite', version: vite.version, - async importEntrypoint(url) { - const baseConfig = await getBaseConfig(); - const envConfig: vite.InlineConfig = { - plugins: [ - wxtPlugins.webextensionPolyfillMock(wxtConfig), - wxtPlugins.removeEntrypointMainFunction(wxtConfig, url), - ], - }; - const config = vite.mergeConfig(baseConfig, envConfig); - const server = await vite.createServer(config); - await server.listen(); - const runtime = await vite.createViteRuntime(server, { hmr: false }); - const module = await runtime.executeUrl(url); - await server.close(); - return module.default; + async importEntrypoint(path) { + switch (wxtConfig.experimental.entrypointImporter) { + default: + case 'jiti': { + return await importEntrypointFile(path); + } + case 'vite-runtime': { + const baseConfig = await getBaseConfig(); + const envConfig: vite.InlineConfig = { + plugins: [ + wxtPlugins.webextensionPolyfillMock(wxtConfig), + wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), + ], + }; + const config = vite.mergeConfig(baseConfig, envConfig); + const server = await vite.createServer(config); + await server.listen(); + const runtime = await vite.createViteRuntime(server, { hmr: false }); + const module = await runtime.executeUrl(path); + await server.close(); + return module.default; + } + case 'vite-node': { + const baseConfig = await getBaseConfig(); + // 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.webextensionPolyfillMock(wxtConfig), + wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), + ], + }; + const config = vite.mergeConfig(baseConfig, envConfig); + const server = await vite.createServer(config); + await server.pluginContainer.buildStart({}); + const node = new ViteNodeServer(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); + await server.close(); + return res.default; + } + } }, async build(group) { let entryConfig; 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 5e4f2a45..79876c93 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 @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { BackgroundEntrypoint, BackgroundEntrypointOptions, @@ -12,16 +12,13 @@ import { import { resolve } from 'path'; import { findEntrypoints } from '../find-entrypoints'; import fs from 'fs-extra'; -import { importEntrypointFile } from '../import-entrypoint'; import glob from 'fast-glob'; import { fakeResolvedConfig, setFakeWxt, } from '~/core/utils/testing/fake-objects'; import { unnormalizePath } from '~/core/utils/paths'; - -vi.mock('../import-entrypoint'); -const importEntrypointFileMock = vi.mocked(importEntrypointFile); +import { wxt } from '~/core/wxt'; vi.mock('fast-glob'); const globMock = vi.mocked(glob); @@ -39,9 +36,11 @@ describe('findEntrypoints', () => { outDir: resolve('.output'), command: 'build', }); + let importEntrypointMock: Mock; beforeEach(() => { setFakeWxt({ config }); + importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint); }); it.each<[string, string, PopupEntrypoint]>([ @@ -211,13 +210,13 @@ describe('findEntrypoints', () => { matches: [''], }; globMock.mockResolvedValueOnce([path]); - importEntrypointFileMock.mockResolvedValue(options); + importEntrypointMock.mockResolvedValue(options); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointMock).toBeCalledWith(expected.inputPath); }, ); @@ -249,13 +248,13 @@ describe('findEntrypoints', () => { type: 'module', }; globMock.mockResolvedValueOnce([path]); - importEntrypointFileMock.mockResolvedValue(options); + importEntrypointMock.mockResolvedValue(options); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointMock).toBeCalledWith(expected.inputPath); }, ); @@ -338,12 +337,13 @@ describe('findEntrypoints', () => { config: { manifestVersion: 2, }, + builder: wxt.builder, }); const options: BackgroundEntrypointOptions = { type: 'module', }; globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointFileMock.mockResolvedValue(options); + importEntrypointMock.mockResolvedValue(options); const entrypoints = await findEntrypoints(); @@ -355,12 +355,13 @@ describe('findEntrypoints', () => { config: { manifestVersion: 3, }, + builder: wxt.builder, }); const options: BackgroundEntrypointOptions = { type: 'module', }; globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointFileMock.mockResolvedValue(options); + importEntrypointMock.mockResolvedValue(options); const entrypoints = await findEntrypoints(); @@ -373,6 +374,7 @@ describe('findEntrypoints', () => { ...config, command: 'serve', }, + builder: wxt.builder, }); globMock.mockResolvedValueOnce(['popup.html']); @@ -410,13 +412,13 @@ describe('findEntrypoints', () => { }; const options: BaseEntrypointOptions = {}; globMock.mockResolvedValueOnce([path]); - importEntrypointFileMock.mockResolvedValue(options); + importEntrypointMock.mockResolvedValue(options); const entrypoints = await findEntrypoints(); expect(entrypoints).toHaveLength(1); expect(entrypoints[0]).toEqual({ ...expected, options }); - expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); + expect(importEntrypointMock).toBeCalledWith(expected.inputPath); }, ); @@ -701,7 +703,7 @@ describe('findEntrypoints', () => { describe('include option', () => { it("should filter out the background when include doesn't contain the target browser", async () => { globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointFileMock.mockResolvedValue({ + importEntrypointMock.mockResolvedValue({ include: ['not' + config.browser], }); @@ -712,7 +714,7 @@ describe('findEntrypoints', () => { it("should filter out content scripts when include doesn't contain the target browser", async () => { globMock.mockResolvedValueOnce(['example.content.ts']); - importEntrypointFileMock.mockResolvedValue({ + importEntrypointMock.mockResolvedValue({ include: ['not' + config.browser], }); @@ -776,7 +778,7 @@ describe('findEntrypoints', () => { describe('exclude option', () => { it('should filter out the background when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['background.ts']); - importEntrypointFileMock.mockResolvedValue({ + importEntrypointMock.mockResolvedValue({ exclude: [config.browser], }); @@ -787,7 +789,7 @@ describe('findEntrypoints', () => { it('should filter out content scripts when exclude contains the target browser', async () => { globMock.mockResolvedValueOnce(['example.content.ts']); - importEntrypointFileMock.mockResolvedValue({ + importEntrypointMock.mockResolvedValue({ exclude: [config.browser], }); @@ -850,7 +852,6 @@ describe('findEntrypoints', () => { 'ui.content/index.ts', 'injected.content/index.ts', ]); - importEntrypointFileMock.mockResolvedValue({}); const filterEntrypoints = ['popup', 'ui']; setFakeWxt({ config: { @@ -860,8 +861,11 @@ describe('findEntrypoints', () => { command: 'build', filterEntrypoints: new Set(filterEntrypoints), }, + builder: wxt.builder, }); + importEntrypointMock.mockResolvedValue({}); + const entrypoints = await findEntrypoints(); const names = entrypoints.map((item) => item.name); expect(names).toHaveLength(2); diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index af23731d..985343a2 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -28,7 +28,6 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants'; import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths'; import pc from 'picocolors'; import { wxt } from '../../wxt'; -import { importEntrypointFile } from './import-entrypoint'; /** * Return entrypoints and their configuration by looking through the project's files. @@ -290,7 +289,7 @@ async function getUnlistedScriptEntrypoint({ skipped, }: EntrypointInfo): Promise { const defaultExport = - await importEntrypoint(inputPath); + await wxt.builder.importEntrypoint(inputPath); if (defaultExport == null) { throw Error( `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`, @@ -315,7 +314,7 @@ async function getBackgroundEntrypoint({ let options: Omit = {}; if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) { const defaultExport = - await importEntrypoint(inputPath); + await wxt.builder.importEntrypoint(inputPath); if (defaultExport == null) { throw Error( `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`, @@ -344,8 +343,15 @@ async function getContentScriptEntrypoint({ name, skipped, }: EntrypointInfo): Promise { - const { main: _, ...options } = - await importEntrypoint(inputPath); + 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(...)"?`, @@ -488,9 +494,3 @@ const PATH_GLOB_TO_TYPE_MAP: Record = { }; const CONTENT_SCRIPT_OUT_DIR = 'content-scripts'; - -function importEntrypoint(path: string) { - return wxt.config.experimental.viteRuntime - ? wxt.builder.importEntrypoint(path) - : importEntrypointFile(path); -} diff --git a/packages/wxt/src/core/utils/building/resolve-config.ts b/packages/wxt/src/core/utils/building/resolve-config.ts index fe3fd68a..ab3278b4 100644 --- a/packages/wxt/src/core/utils/building/resolve-config.ts +++ b/packages/wxt/src/core/utils/building/resolve-config.ts @@ -167,7 +167,7 @@ export async function resolveConfig( alias, experimental: defu(mergedConfig.experimental, { includeBrowserPolyfill: true, - viteRuntime: false, + entrypointImporter: 'jiti' as const, }), dev: { server: devServerConfig, diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 08c13385..45d18e0e 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -297,7 +297,7 @@ export const fakeResolvedConfig = fakeObjectCreator(() => { alias: {}, experimental: { includeBrowserPolyfill: true, - viteRuntime: false, + entrypointImporter: 'jiti', }, dev: { reloadCommand: 'Alt+R', diff --git a/packages/wxt/src/types/index.ts b/packages/wxt/src/types/index.ts index 3bf0063b..65a965b8 100644 --- a/packages/wxt/src/types/index.ts +++ b/packages/wxt/src/types/index.ts @@ -317,14 +317,17 @@ export interface InlineConfig { */ includeBrowserPolyfill?: boolean; /** - * When set to `true`, use the Vite Runtime API to load entrypoint options instead of the default, `jiti`. + * Method used to import entrypoint files during the build process to extract their options. * - * Lets you use imported variables and leverage your Vite config to add support for non-standard APIs/syntax. + * - "jiti": Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function + * - "vite-runtime" (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file + * - "vite-node" (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file * - * @experimental Early access to try out the feature before it becomes the default. - * @default false + * @see {@link https://wxt.dev/guide/go-further/entrypoint-side-effects.html|Entrypoint Side-effect Docs} + * + * @default "jiti" */ - viteRuntime?: boolean; + entrypointImporter?: 'jiti' | 'vite-runtime' | 'vite-node'; }; /** * Config effecting dev mode only. @@ -1174,7 +1177,7 @@ export interface ResolvedConfig { alias: Record; experimental: { includeBrowserPolyfill: boolean; - viteRuntime: boolean; + entrypointImporter: 'jiti' | 'vite-runtime' | 'vite-node'; }; dev: { /** Only defined during dev command */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ea6120c..f239151a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,6 +291,9 @@ importers: vite: specifier: ^5.3.0 version: 5.3.0(@types/node@20.14.2) + vite-node: + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.14.2) web-ext-run: specifier: ^0.2.0 version: 0.2.0 @@ -741,6 +744,7 @@ packages: /@babel/highlight@7.24.7: resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} + requiresBuild: true dependencies: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 @@ -7028,7 +7032,7 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-node@1.6.0: + /vite-node@1.6.0(@types/node@20.14.2): resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -7047,7 +7051,6 @@ packages: - sugarss - supports-color - terser - dev: true /vite-plugin-solid@2.10.2(solid-js@1.8.17)(vite@5.3.0): resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} @@ -7235,7 +7238,7 @@ packages: tinybench: 2.5.1 tinypool: 0.8.4 vite: 5.3.0(@types/node@20.14.2) - vite-node: 1.6.0 + vite-node: 1.6.0(@types/node@20.14.2) why-is-node-running: 2.2.2 transitivePeerDependencies: - less