fix: Import entrypoint improvements (#1207)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -36,11 +36,12 @@ describe('findEntrypoints', () => {
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
let importEntrypointMock: Mock;
|
||||
let importEntrypointsMock: Mock<typeof wxt.builder.importEntrypoints>;
|
||||
|
||||
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: ['<all_urls>'],
|
||||
};
|
||||
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();
|
||||
|
||||
|
||||
@@ -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<Entrypoint[]> {
|
||||
|
||||
// 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<Entrypoint> => {
|
||||
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<Entrypoint> => {
|
||||
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<string, Record<string, any> | 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<Record<string, any>> {
|
||||
const content = await fs.readFile(info.inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const metaTags = document.querySelectorAll('meta');
|
||||
const res: Record<string, any> = {
|
||||
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<Record<string, string[]>>(
|
||||
(map, { name, inputPath }) => {
|
||||
@@ -200,32 +254,26 @@ function preventNoEntrypoints(files: EntrypointInfo[]) {
|
||||
|
||||
async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
options: Record<string, any>,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
|
||||
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<string, any>,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
|
||||
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<string, any>,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<BaseEntrypointOptions>(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<GenericEntrypoint> {
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(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<string, any>,
|
||||
): Promise<GenericEntrypoint> {
|
||||
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<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<BackgroundDefinition>(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<string, any>,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
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<ContentScriptEntrypoint> {
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<ContentScriptDefinition>(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<string, any>,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
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<string, any>,
|
||||
): Promise<SidepanelEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<SidepanelEntrypointOptions>(
|
||||
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<T extends BaseEntrypointOptions>(
|
||||
info: EntrypointInfo,
|
||||
keyMap: Record<keyof T, string>,
|
||||
queries?: Partial<{
|
||||
[key in keyof T]: (
|
||||
document: Document,
|
||||
manifestKey: string,
|
||||
) => string | undefined;
|
||||
}>,
|
||||
parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>,
|
||||
): Promise<T> {
|
||||
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<Entrypoint, 'skipped'>): boolean {
|
||||
if (wxt.config.filterEntrypoints != null) {
|
||||
return !wxt.config.filterEntrypoints.has(entry.name);
|
||||
|
||||
@@ -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<Entrypoint, 'inputPath'>,
|
||||
): 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<Entrypoint, 'inputPath'>,
|
||||
): boolean {
|
||||
const ext = extname(entrypoint.inputPath);
|
||||
return ['.js', '.jsx', '.ts', '.tsx'].includes(ext);
|
||||
}
|
||||
|
||||
@@ -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<T>(path: string): Promise<T>;
|
||||
/**
|
||||
* Import a list of JS entrypoint files, returning their options.
|
||||
*/
|
||||
importEntrypoints(paths: string[]): Promise<Record<string, unknown>[]>;
|
||||
/**
|
||||
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
|
||||
* build process.
|
||||
|
||||
Reference in New Issue
Block a user