wxt 0.19.19 → 0.19.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,9 @@
1
1
  import { browser } from "wxt/browser";
2
+ import { waitElement } from "@1natsu/wait-element";
3
+ import {
4
+ isExist as mountDetector,
5
+ isNotExist as removeDetector
6
+ } from "@1natsu/wait-element/detectors";
2
7
  import { logger } from "../../../sandbox/utils/logger.mjs";
3
8
  import { createIsolatedElement } from "@webext-core/isolated-element";
4
9
  export * from "./types.mjs";
@@ -17,14 +22,20 @@ export function createIntegratedUi(ctx, options) {
17
22
  wrapper.remove();
18
23
  mounted = void 0;
19
24
  };
25
+ const mountFunctions = createMountFunctions(
26
+ {
27
+ mount,
28
+ remove
29
+ },
30
+ options
31
+ );
20
32
  ctx.onInvalidated(remove);
21
33
  return {
22
34
  get mounted() {
23
35
  return mounted;
24
36
  },
25
37
  wrapper,
26
- mount,
27
- remove
38
+ ...mountFunctions
28
39
  };
29
40
  }
30
41
  export function createIframeUi(ctx, options) {
@@ -44,6 +55,13 @@ export function createIframeUi(ctx, options) {
44
55
  wrapper.remove();
45
56
  mounted = void 0;
46
57
  };
58
+ const mountFunctions = createMountFunctions(
59
+ {
60
+ mount,
61
+ remove
62
+ },
63
+ options
64
+ );
47
65
  ctx.onInvalidated(remove);
48
66
  return {
49
67
  get mounted() {
@@ -51,8 +69,7 @@ export function createIframeUi(ctx, options) {
51
69
  },
52
70
  iframe,
53
71
  wrapper,
54
- mount,
55
- remove
72
+ ...mountFunctions
56
73
  };
57
74
  }
58
75
  export async function createShadowRootUi(ctx, options) {
@@ -87,13 +104,19 @@ export async function createShadowRootUi(ctx, options) {
87
104
  uiContainer.removeChild(uiContainer.lastChild);
88
105
  mounted = void 0;
89
106
  };
107
+ const mountFunctions = createMountFunctions(
108
+ {
109
+ mount,
110
+ remove
111
+ },
112
+ options
113
+ );
90
114
  ctx.onInvalidated(remove);
91
115
  return {
92
116
  shadow,
93
117
  shadowHost,
94
118
  uiContainer,
95
- mount,
96
- remove,
119
+ ...mountFunctions,
97
120
  get mounted() {
98
121
  return mounted;
99
122
  }
@@ -172,6 +195,81 @@ function mountUi(root, options) {
172
195
  break;
173
196
  }
174
197
  }
198
+ function createMountFunctions(baseFunctions, options) {
199
+ let autoMountInstance = void 0;
200
+ const stopAutoMount = () => {
201
+ autoMountInstance?.stopAutoMount();
202
+ autoMountInstance = void 0;
203
+ };
204
+ const mount = () => {
205
+ baseFunctions.mount();
206
+ };
207
+ const unmount = baseFunctions.remove;
208
+ const remove = () => {
209
+ stopAutoMount();
210
+ baseFunctions.remove();
211
+ };
212
+ const autoMount = (autoMountOptions) => {
213
+ if (autoMountInstance) {
214
+ logger.warn("autoMount is already set.");
215
+ }
216
+ autoMountInstance = autoMountUi(
217
+ { mount, unmount, stopAutoMount },
218
+ {
219
+ ...options,
220
+ ...autoMountOptions
221
+ }
222
+ );
223
+ };
224
+ return {
225
+ mount,
226
+ remove,
227
+ autoMount
228
+ };
229
+ }
230
+ function autoMountUi(uiCallbacks, options) {
231
+ const abortController = new AbortController();
232
+ const EXPLICIT_STOP_REASON = "explicit_stop_auto_mount";
233
+ const _stopAutoMount = () => {
234
+ abortController.abort(EXPLICIT_STOP_REASON);
235
+ options.onStop?.();
236
+ };
237
+ let resolvedAnchor = typeof options.anchor === "function" ? options.anchor() : options.anchor;
238
+ if (resolvedAnchor instanceof Element) {
239
+ throw Error(
240
+ "autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor."
241
+ );
242
+ }
243
+ async function observeElement(selector) {
244
+ let isAnchorExist = !!getAnchor(options);
245
+ while (!abortController.signal.aborted) {
246
+ try {
247
+ const changedAnchor = await waitElement(selector ?? "body", {
248
+ customMatcher: () => getAnchor(options) ?? null,
249
+ detector: isAnchorExist ? removeDetector : mountDetector,
250
+ signal: abortController.signal
251
+ });
252
+ isAnchorExist = !!changedAnchor;
253
+ if (isAnchorExist) {
254
+ uiCallbacks.mount();
255
+ } else {
256
+ uiCallbacks.unmount();
257
+ if (options.once) {
258
+ uiCallbacks.stopAutoMount();
259
+ }
260
+ }
261
+ } catch (error) {
262
+ if (abortController.signal.aborted && abortController.signal.reason === EXPLICIT_STOP_REASON) {
263
+ break;
264
+ } else {
265
+ throw error;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ observeElement(resolvedAnchor);
271
+ return { stopAutoMount: _stopAutoMount };
272
+ }
175
273
  async function loadCss() {
176
274
  const url = browser.runtime.getURL(
177
275
  `/content-scripts/${import.meta.env.ENTRYPOINT}.css`
@@ -30,18 +30,7 @@ export interface ShadowRootContentScriptUi<TMounted> extends ContentScriptUi<TMo
30
30
  */
31
31
  shadow: ShadowRoot;
32
32
  }
33
- export interface ContentScriptUi<TMounted> {
34
- /**
35
- * Function that mounts or remounts the UI on the page.
36
- */
37
- mount: () => void;
38
- /**
39
- * Function that removes the UI from the webpage.
40
- */
41
- remove: () => void;
42
- /**>
43
- * Custom data returned from the `options.mount` function.
44
- */
33
+ export interface ContentScriptUi<TMounted> extends MountFunctions {
45
34
  mounted: TMounted | undefined;
46
35
  }
47
36
  export type ContentScriptUiOptions<TMounted> = ContentScriptPositioningOptions & ContentScriptAnchoredOptions & {
@@ -172,3 +161,36 @@ export interface ContentScriptAnchoredOptions {
172
161
  */
173
162
  append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
174
163
  }
164
+ export interface BaseMountFunctions {
165
+ /**
166
+ * Function that mounts or remounts the UI on the page.
167
+ */
168
+ mount: () => void;
169
+ /**
170
+ * Function that removes the UI from the webpage.
171
+ */
172
+ remove: () => void;
173
+ }
174
+ export interface MountFunctions extends BaseMountFunctions {
175
+ /**
176
+ * Call `ui.autoMount()` to automatically mount and remove the UI as the anchor is dynamically added/removed by the webpage.
177
+ */
178
+ autoMount: (options?: AutoMountOptions) => void;
179
+ }
180
+ export type AutoMountOptions = {
181
+ /**
182
+ * When true, only mount and unmount a UI once.
183
+ */
184
+ once?: boolean;
185
+ /**
186
+ * The callback triggered when `StopAutoMount` is called.
187
+ */
188
+ onStop?: () => void;
189
+ };
190
+ export type StopAutoMount = () => void;
191
+ export interface AutoMount {
192
+ /**
193
+ * Stop watching the anchor element for changes, but keep the UI mounted.
194
+ */
195
+ stopAutoMount: StopAutoMount;
196
+ }
@@ -9,6 +9,8 @@ import { importEntrypointFile } from "../../utils/building/index.mjs";
9
9
  import { ViteNodeServer } from "vite-node/server";
10
10
  import { ViteNodeRunner } from "vite-node/client";
11
11
  import { installSourcemapsSupport } from "vite-node/source-map";
12
+ import { createExtensionEnvironment } from "../../utils/environments/index.mjs";
13
+ import { relative } from "node:path";
12
14
  export async function createViteBuilder(wxtConfig, hooks, getWxtDevServer) {
13
15
  const vite = await import("vite");
14
16
  const getBaseConfig = async (baseConfigOptions) => {
@@ -161,54 +163,96 @@ export async function createViteBuilder(wxtConfig, hooks, getWxtDevServer) {
161
163
  }
162
164
  };
163
165
  };
166
+ const createViteNodeImporter = async (paths) => {
167
+ const baseConfig = await getBaseConfig({
168
+ excludeAnalysisPlugin: true
169
+ });
170
+ baseConfig.optimizeDeps ??= {};
171
+ baseConfig.optimizeDeps.noDiscovery = true;
172
+ baseConfig.optimizeDeps.include = [];
173
+ const envConfig = {
174
+ plugins: paths.map(
175
+ (path) => wxtPlugins.removeEntrypointMainFunction(wxtConfig, path)
176
+ )
177
+ };
178
+ const config = vite.mergeConfig(baseConfig, envConfig);
179
+ const server = await vite.createServer(config);
180
+ await server.pluginContainer.buildStart({});
181
+ const node = new ViteNodeServer(
182
+ // @ts-ignore: Some weird type error...
183
+ server
184
+ );
185
+ installSourcemapsSupport({
186
+ getSourceMap: (source) => node.getSourceMap(source)
187
+ });
188
+ const runner = new ViteNodeRunner({
189
+ root: server.config.root,
190
+ base: server.config.base,
191
+ // when having the server and runner in a different context,
192
+ // you will need to handle the communication between them
193
+ // and pass to this function
194
+ fetchModule(id) {
195
+ return node.fetchModule(id);
196
+ },
197
+ resolveId(id, importer) {
198
+ return node.resolveId(id, importer);
199
+ }
200
+ });
201
+ return { runner, server };
202
+ };
203
+ const requireDefaultExport = (path, mod) => {
204
+ const relativePath = relative(wxtConfig.root, path);
205
+ if (mod?.default == null) {
206
+ const defineFn = relativePath.includes(".content") ? "defineContentScript" : relativePath.includes("background") ? "defineBackground" : "defineUnlistedScript";
207
+ throw Error(
208
+ `${relativePath}: Default export not found, did you forget to call "export default ${defineFn}(...)"?`
209
+ );
210
+ }
211
+ };
164
212
  return {
165
213
  name: "Vite",
166
214
  version: vite.version,
167
215
  async importEntrypoint(path) {
216
+ const env = createExtensionEnvironment();
168
217
  switch (wxtConfig.entrypointLoader) {
169
218
  default:
170
219
  case "jiti": {
171
- return await importEntrypointFile(path);
220
+ return await env.run(() => importEntrypointFile(path));
172
221
  }
173
222
  case "vite-node": {
174
- const baseConfig = await getBaseConfig({
175
- excludeAnalysisPlugin: true
176
- });
177
- baseConfig.optimizeDeps ??= {};
178
- baseConfig.optimizeDeps.noDiscovery = true;
179
- baseConfig.optimizeDeps.include = [];
180
- const envConfig = {
181
- plugins: [wxtPlugins.removeEntrypointMainFunction(wxtConfig, path)]
182
- };
183
- const config = vite.mergeConfig(baseConfig, envConfig);
184
- const server = await vite.createServer(config);
185
- await server.pluginContainer.buildStart({});
186
- const node = new ViteNodeServer(
187
- // @ts-ignore: Some weird type error...
188
- server
189
- );
190
- installSourcemapsSupport({
191
- getSourceMap: (source) => node.getSourceMap(source)
192
- });
193
- const runner = new ViteNodeRunner({
194
- root: server.config.root,
195
- base: server.config.base,
196
- // when having the server and runner in a different context,
197
- // you will need to handle the communication between them
198
- // and pass to this function
199
- fetchModule(id) {
200
- return node.fetchModule(id);
201
- },
202
- resolveId(id, importer) {
203
- return node.resolveId(id, importer);
204
- }
205
- });
206
- const res = await runner.executeFile(path);
223
+ const { runner, server } = await createViteNodeImporter([path]);
224
+ const res = await env.run(() => runner.executeFile(path));
207
225
  await server.close();
226
+ requireDefaultExport(path, res);
208
227
  return res.default;
209
228
  }
210
229
  }
211
230
  },
231
+ async importEntrypoints(paths) {
232
+ const env = createExtensionEnvironment();
233
+ switch (wxtConfig.entrypointLoader) {
234
+ default:
235
+ case "jiti": {
236
+ return await env.run(
237
+ () => Promise.all(paths.map(importEntrypointFile))
238
+ );
239
+ }
240
+ case "vite-node": {
241
+ const { runner, server } = await createViteNodeImporter(paths);
242
+ const res = await env.run(
243
+ () => Promise.all(
244
+ paths.map(async (path) => {
245
+ const mod = await runner.executeFile(path);
246
+ requireDefaultExport(path, mod);
247
+ return mod.default;
248
+ })
249
+ )
250
+ );
251
+ await server.close();
252
+ return res;
253
+ }
254
+ }
255
+ },
212
256
  async build(group) {
213
257
  let entryConfig;
214
258
  if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
@@ -76,7 +76,9 @@ async function createServerInternal() {
76
76
  const reloadOnChange = createFileReloader(server);
77
77
  server.watcher.on("all", reloadOnChange);
78
78
  keyboardShortcuts.start();
79
- keyboardShortcuts.printHelp();
79
+ keyboardShortcuts.printHelp({
80
+ canReopenBrowser: !wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.()
81
+ });
80
82
  },
81
83
  async stop() {
82
84
  wasStopped = true;
@@ -2,7 +2,9 @@ import { WxtDevServer } from '../types';
2
2
  export interface KeyboardShortcutWatcher {
3
3
  start(): void;
4
4
  stop(): void;
5
- printHelp(): void;
5
+ printHelp(flags: {
6
+ canReopenBrowser: boolean;
7
+ }): void;
6
8
  }
7
9
  /**
8
10
  * Function that creates a keyboard shortcut handler for the extension.
@@ -28,8 +28,8 @@ export function createKeyboardShortcuts(server) {
28
28
  }
29
29
  isWatching = false;
30
30
  },
31
- printHelp() {
32
- if (!wxt.config.runnerConfig.config.disabled) {
31
+ printHelp(flags) {
32
+ if (flags.canReopenBrowser) {
33
33
  wxt.logger.info(
34
34
  `${pc.dim("Press")} ${pc.bold("o + enter")} ${pc.dim("to reopen the browser")}`
35
35
  );
@@ -4,6 +4,9 @@ import { wxt } from "../wxt.mjs";
4
4
  export function createWebExtRunner() {
5
5
  let runner;
6
6
  return {
7
+ canOpen() {
8
+ return true;
9
+ },
7
10
  async openBrowser() {
8
11
  const startTime = Date.now();
9
12
  if (wxt.config.browser === "firefox" && wxt.config.manifestVersion === 3) {
@@ -6,13 +6,15 @@ import JSON5 from "json5";
6
6
  import glob from "fast-glob";
7
7
  import {
8
8
  getEntrypointName,
9
+ isHtmlEntrypoint,
10
+ isJsEntrypoint,
9
11
  resolvePerBrowserOptions
10
12
  } from "../../utils/entrypoints.mjs";
11
13
  import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from "../../utils/constants.mjs";
12
14
  import { CSS_EXTENSIONS_PATTERN } from "../../utils/paths.mjs";
13
15
  import pc from "picocolors";
14
16
  import { wxt } from "../../wxt.mjs";
15
- import { createExtensionEnvironment } from "../environments/index.mjs";
17
+ import { camelCase } from "scule";
16
18
  export async function findEntrypoints() {
17
19
  await fs.mkdir(wxt.config.wxtDir, { recursive: true });
18
20
  try {
@@ -46,58 +48,60 @@ export async function findEntrypoints() {
46
48
  preventNoEntrypoints(entrypointInfos);
47
49
  preventDuplicateEntrypointNames(entrypointInfos);
48
50
  let hasBackground = false;
49
- const env = createExtensionEnvironment();
50
- const entrypointsWithoutSkipped = await env.run(
51
- () => Promise.all(
52
- entrypointInfos.map(async (info) => {
53
- const { type } = info;
54
- switch (type) {
55
- case "popup":
56
- return await getPopupEntrypoint(info);
57
- case "sidepanel":
58
- return await getSidepanelEntrypoint(info);
59
- case "options":
60
- return await getOptionsEntrypoint(info);
61
- case "background":
62
- hasBackground = true;
63
- return await getBackgroundEntrypoint(info);
64
- case "content-script":
65
- return await getContentScriptEntrypoint(info);
66
- case "unlisted-page":
67
- return await getUnlistedPageEntrypoint(info);
68
- case "unlisted-script":
69
- return await getUnlistedScriptEntrypoint(info);
70
- case "content-script-style":
71
- return {
72
- ...info,
73
- type,
74
- outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
75
- options: {
76
- include: void 0,
77
- exclude: void 0
78
- }
79
- };
80
- default:
81
- return {
82
- ...info,
83
- type,
84
- outputDir: wxt.config.outDir,
85
- options: {
86
- include: void 0,
87
- exclude: void 0
88
- }
89
- };
90
- }
91
- })
92
- )
51
+ const entrypointOptions = await importEntrypoints(entrypointInfos);
52
+ const entrypointsWithoutSkipped = await Promise.all(
53
+ entrypointInfos.map(async (info) => {
54
+ const { type } = info;
55
+ const options = entrypointOptions[info.inputPath] ?? {};
56
+ switch (type) {
57
+ case "popup":
58
+ return await getPopupEntrypoint(info, options);
59
+ case "sidepanel":
60
+ return await getSidepanelEntrypoint(info, options);
61
+ case "options":
62
+ return await getOptionsEntrypoint(info, options);
63
+ case "background":
64
+ hasBackground = true;
65
+ return await getBackgroundEntrypoint(info, options);
66
+ case "content-script":
67
+ return await getContentScriptEntrypoint(info, options);
68
+ case "unlisted-page":
69
+ return await getUnlistedPageEntrypoint(info, options);
70
+ case "unlisted-script":
71
+ return await getUnlistedScriptEntrypoint(info, options);
72
+ case "content-script-style":
73
+ return {
74
+ ...info,
75
+ type,
76
+ outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
77
+ options: {
78
+ include: options.include,
79
+ exclude: options.exclude
80
+ }
81
+ };
82
+ default:
83
+ return {
84
+ ...info,
85
+ type,
86
+ outputDir: wxt.config.outDir,
87
+ options: {
88
+ include: options.include,
89
+ exclude: options.exclude
90
+ }
91
+ };
92
+ }
93
+ })
93
94
  );
94
95
  if (wxt.config.command === "serve" && !hasBackground) {
95
96
  entrypointsWithoutSkipped.push(
96
- await getBackgroundEntrypoint({
97
- inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
98
- name: "background",
99
- type: "background"
100
- })
97
+ await getBackgroundEntrypoint(
98
+ {
99
+ inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
100
+ name: "background",
101
+ type: "background"
102
+ },
103
+ {}
104
+ )
101
105
  );
102
106
  }
103
107
  const entrypoints = entrypointsWithoutSkipped.map((entry) => ({
@@ -119,6 +123,48 @@ export async function findEntrypoints() {
119
123
  await wxt.hooks.callHook("entrypoints:resolved", wxt, entrypoints);
120
124
  return entrypoints;
121
125
  }
126
+ async function importEntrypoints(infos) {
127
+ const resMap = {};
128
+ const htmlInfos = infos.filter((info) => isHtmlEntrypoint(info));
129
+ const jsInfos = infos.filter((info) => isJsEntrypoint(info));
130
+ await Promise.all([
131
+ // HTML
132
+ ...htmlInfos.map(async (info) => {
133
+ const res = await importHtmlEntrypoint(info);
134
+ resMap[info.inputPath] = res;
135
+ }),
136
+ // JS
137
+ (async () => {
138
+ const res = await wxt.builder.importEntrypoints(
139
+ jsInfos.map((info) => info.inputPath)
140
+ );
141
+ res.forEach((res2, i) => {
142
+ resMap[jsInfos[i].inputPath] = res2;
143
+ });
144
+ })()
145
+ // CSS - never has options
146
+ ]);
147
+ return resMap;
148
+ }
149
+ async function importHtmlEntrypoint(info) {
150
+ const content = await fs.readFile(info.inputPath, "utf-8");
151
+ const { document } = parseHTML(content);
152
+ const metaTags = document.querySelectorAll("meta");
153
+ const res = {
154
+ title: document.querySelector("title")?.textContent || void 0
155
+ };
156
+ metaTags.forEach((tag) => {
157
+ const name = tag.name;
158
+ if (!name.startsWith("manifest.")) return;
159
+ const key = camelCase(name.slice(9));
160
+ try {
161
+ res[key] = JSON5.parse(tag.content);
162
+ } catch {
163
+ res[key] = tag.content;
164
+ }
165
+ });
166
+ return res;
167
+ }
122
168
  function preventDuplicateEntrypointNames(files) {
123
169
  const namesToPaths = files.reduce(
124
170
  (map, { name, inputPath }) => {
@@ -154,181 +200,125 @@ function preventNoEntrypoints(files) {
154
200
  throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
155
201
  }
156
202
  }
157
- async function getPopupEntrypoint(info) {
158
- const options = await getHtmlEntrypointOptions(
159
- info,
203
+ async function getPopupEntrypoint(info, options) {
204
+ const stictOptions = resolvePerBrowserOptions(
160
205
  {
161
- browserStyle: "browser_style",
162
- exclude: "exclude",
163
- include: "include",
164
- defaultIcon: "default_icon",
165
- defaultTitle: "default_title",
166
- mv2Key: "type"
206
+ browserStyle: options.browserStyle,
207
+ exclude: options.exclude,
208
+ include: options.include,
209
+ defaultIcon: options.defaultIcon,
210
+ defaultTitle: options.title,
211
+ mv2Key: options.type
167
212
  },
168
- {
169
- defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
170
- },
171
- {
172
- defaultTitle: (content) => content,
173
- mv2Key: (content) => content === "page_action" ? "page_action" : "browser_action"
174
- }
213
+ wxt.config.browser
175
214
  );
215
+ if (stictOptions.mv2Key && stictOptions.mv2Key !== "page_action")
216
+ stictOptions.mv2Key = "browser_action";
176
217
  return {
177
218
  type: "popup",
178
219
  name: "popup",
179
- options: resolvePerBrowserOptions(options, wxt.config.browser),
220
+ options: stictOptions,
180
221
  inputPath: info.inputPath,
181
222
  outputDir: wxt.config.outDir
182
223
  };
183
224
  }
184
- async function getOptionsEntrypoint(info) {
185
- const options = await getHtmlEntrypointOptions(
186
- info,
187
- {
188
- browserStyle: "browser_style",
189
- chromeStyle: "chrome_style",
190
- exclude: "exclude",
191
- include: "include",
192
- openInTab: "open_in_tab"
193
- }
194
- );
225
+ async function getOptionsEntrypoint(info, options) {
195
226
  return {
196
227
  type: "options",
197
228
  name: "options",
198
- options: resolvePerBrowserOptions(options, wxt.config.browser),
229
+ options: resolvePerBrowserOptions(
230
+ {
231
+ browserStyle: options.browserStyle,
232
+ chromeStyle: options.chromeStyle,
233
+ exclude: options.exclude,
234
+ include: options.include,
235
+ openInTab: options.openInTab
236
+ },
237
+ wxt.config.browser
238
+ ),
199
239
  inputPath: info.inputPath,
200
240
  outputDir: wxt.config.outDir
201
241
  };
202
242
  }
203
- async function getUnlistedPageEntrypoint(info) {
204
- const options = await getHtmlEntrypointOptions(info, {
205
- exclude: "exclude",
206
- include: "include"
207
- });
243
+ async function getUnlistedPageEntrypoint(info, options) {
208
244
  return {
209
245
  type: "unlisted-page",
210
246
  name: info.name,
211
247
  inputPath: info.inputPath,
212
248
  outputDir: wxt.config.outDir,
213
- options
249
+ options: {
250
+ include: options.include,
251
+ exclude: options.exclude
252
+ }
214
253
  };
215
254
  }
216
- async function getUnlistedScriptEntrypoint({
217
- inputPath,
218
- name
219
- }) {
220
- const defaultExport = await wxt.builder.importEntrypoint(inputPath);
221
- if (defaultExport == null) {
222
- throw Error(
223
- `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`
224
- );
225
- }
226
- const { main: _, ...options } = defaultExport;
255
+ async function getUnlistedScriptEntrypoint({ inputPath, name }, options) {
227
256
  return {
228
257
  type: "unlisted-script",
229
258
  name,
230
259
  inputPath,
231
260
  outputDir: wxt.config.outDir,
232
- options: resolvePerBrowserOptions(options, wxt.config.browser)
261
+ options: resolvePerBrowserOptions(
262
+ {
263
+ include: options.include,
264
+ exclude: options.exclude
265
+ },
266
+ wxt.config.browser
267
+ )
233
268
  };
234
269
  }
235
- async function getBackgroundEntrypoint({
236
- inputPath,
237
- name
238
- }) {
239
- let options = {};
240
- if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
241
- const defaultExport = await wxt.builder.importEntrypoint(inputPath);
242
- if (defaultExport == null) {
243
- throw Error(
244
- `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`
245
- );
246
- }
247
- const { main: _, ...moduleOptions } = defaultExport;
248
- options = moduleOptions;
249
- }
270
+ async function getBackgroundEntrypoint({ inputPath, name }, options) {
271
+ const strictOptions = resolvePerBrowserOptions(
272
+ {
273
+ include: options.include,
274
+ exclude: options.exclude,
275
+ persistent: options.persistent,
276
+ type: options.type
277
+ },
278
+ wxt.config.browser
279
+ );
250
280
  if (wxt.config.manifestVersion !== 3) {
251
- delete options.type;
281
+ delete strictOptions.type;
252
282
  }
253
283
  return {
254
284
  type: "background",
255
285
  name,
256
286
  inputPath,
257
287
  outputDir: wxt.config.outDir,
258
- options: resolvePerBrowserOptions(options, wxt.config.browser)
288
+ options: strictOptions
259
289
  };
260
290
  }
261
- async function getContentScriptEntrypoint({
262
- inputPath,
263
- name
264
- }) {
265
- const defaultExport = await wxt.builder.importEntrypoint(inputPath);
266
- if (defaultExport == null) {
267
- throw Error(
268
- `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`
269
- );
270
- }
271
- const { main: _, ...options } = defaultExport;
272
- if (options == null) {
273
- throw Error(
274
- `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`
275
- );
276
- }
291
+ async function getContentScriptEntrypoint({ inputPath, name }, options) {
277
292
  return {
278
293
  type: "content-script",
279
294
  name,
280
295
  inputPath,
281
296
  outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
282
- options: resolvePerBrowserOptions(options, wxt.config.browser)
297
+ options: resolvePerBrowserOptions(
298
+ options,
299
+ wxt.config.browser
300
+ )
283
301
  };
284
302
  }
285
- async function getSidepanelEntrypoint(info) {
286
- const options = await getHtmlEntrypointOptions(
287
- info,
288
- {
289
- browserStyle: "browser_style",
290
- exclude: "exclude",
291
- include: "include",
292
- defaultIcon: "default_icon",
293
- defaultTitle: "default_title",
294
- openAtInstall: "open_at_install"
295
- },
296
- {
297
- defaultTitle: (document) => document.querySelector("title")?.textContent || void 0
298
- },
299
- {
300
- defaultTitle: (content) => content
301
- }
302
- );
303
+ async function getSidepanelEntrypoint(info, options) {
303
304
  return {
304
305
  type: "sidepanel",
305
306
  name: info.name,
306
- options: resolvePerBrowserOptions(options, wxt.config.browser),
307
+ options: resolvePerBrowserOptions(
308
+ {
309
+ browserStyle: options.browserStyle,
310
+ exclude: options.exclude,
311
+ include: options.include,
312
+ defaultIcon: options.defaultIcon,
313
+ defaultTitle: options.title,
314
+ openAtInstall: options.openAtInstall
315
+ },
316
+ wxt.config.browser
317
+ ),
307
318
  inputPath: info.inputPath,
308
319
  outputDir: wxt.config.outDir
309
320
  };
310
321
  }
311
- async function getHtmlEntrypointOptions(info, keyMap, queries, parsers) {
312
- const content = await fs.readFile(info.inputPath, "utf-8");
313
- const { document } = parseHTML(content);
314
- const options = {};
315
- const defaultQuery = (manifestKey) => document.querySelector(`meta[name='manifest.${manifestKey}']`)?.getAttribute("content");
316
- Object.entries(keyMap).forEach(([_key, manifestKey]) => {
317
- const key = _key;
318
- const content2 = queries?.[key] ? queries[key](document, manifestKey) : defaultQuery(manifestKey);
319
- if (content2) {
320
- try {
321
- options[key] = (parsers?.[key] ?? JSON5.parse)(content2);
322
- } catch (err) {
323
- wxt.logger.fatal(
324
- `Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content2})`,
325
- err
326
- );
327
- }
328
- }
329
- });
330
- return options;
331
- }
332
322
  function isEntrypointSkipped(entry) {
333
323
  if (wxt.config.filterEntrypoints != null) {
334
324
  return !wxt.config.filterEntrypoints.has(entry.name);
@@ -22,4 +22,10 @@ export declare function resolvePerBrowserOptions<T extends Record<string, any>,
22
22
  *
23
23
  * Naively just checking the file extension of the input path.
24
24
  */
25
- export declare function isHtmlEntrypoint(entrypoint: Entrypoint): boolean;
25
+ export declare function isHtmlEntrypoint(entrypoint: Pick<Entrypoint, 'inputPath'>): boolean;
26
+ /**
27
+ * Returns true when the entrypoint is a JS entrypoint.
28
+ *
29
+ * Naively just checking the file extension of the input path.
30
+ */
31
+ export declare function isJsEntrypoint(entrypoint: Pick<Entrypoint, 'inputPath'>): boolean;
@@ -1,4 +1,4 @@
1
- import path, { relative, resolve } from "node:path";
1
+ import path, { relative, resolve, extname } from "node:path";
2
2
  import { normalizePath } from "./paths.mjs";
3
3
  export function getEntrypointName(entrypointsDir, inputPath) {
4
4
  const relativePath = path.relative(entrypointsDir, inputPath);
@@ -27,5 +27,10 @@ export function resolvePerBrowserOptions(options, browser) {
27
27
  );
28
28
  }
29
29
  export function isHtmlEntrypoint(entrypoint) {
30
- return entrypoint.inputPath.endsWith(".html");
30
+ const ext = extname(entrypoint.inputPath);
31
+ return [".html"].includes(ext);
32
+ }
33
+ export function isJsEntrypoint(entrypoint) {
34
+ const ext = extname(entrypoint.inputPath);
35
+ return [".js", ".jsx", ".ts", ".tsx"].includes(ext);
31
36
  }
@@ -7,7 +7,7 @@ export declare function writeManifest(manifest: Manifest.WebExtensionManifest, o
7
7
  /**
8
8
  * Generates the manifest based on the config and entrypoints.
9
9
  */
10
- export declare function generateManifest(entrypoints: Entrypoint[], buildOutput: Omit<BuildOutput, 'manifest'>): Promise<{
10
+ export declare function generateManifest(allEntrypoints: Entrypoint[], buildOutput: Omit<BuildOutput, 'manifest'>): Promise<{
11
11
  manifest: Manifest.WebExtensionManifest;
12
12
  warnings: any[][];
13
13
  }>;
@@ -20,7 +20,8 @@ export async function writeManifest(manifest, output) {
20
20
  fileName: "manifest.json"
21
21
  });
22
22
  }
23
- export async function generateManifest(entrypoints, buildOutput) {
23
+ export async function generateManifest(allEntrypoints, buildOutput) {
24
+ const entrypoints = allEntrypoints.filter((entry) => !entry.skipped);
24
25
  const warnings = [];
25
26
  const pkg = await getPackageJson();
26
27
  let versionName = wxt.config.manifest.version_name ?? wxt.config.manifest.version ?? pkg?.version;
@@ -1,12 +1,12 @@
1
1
  import type { Manifest } from 'wxt/browser';
2
- import { ResolvedConfig, WxtDevServer, BackgroundEntrypoint, ContentScriptEntrypoint, GenericEntrypoint, OptionsEntrypoint, PopupEntrypoint, OutputChunk, OutputFile, OutputAsset, BuildOutput, BuildStepOutput, UserManifest, Wxt, SidepanelEntrypoint } from '../../../types';
2
+ import { ResolvedConfig, WxtDevServer, BackgroundEntrypoint, ContentScriptEntrypoint, GenericEntrypoint, OptionsEntrypoint, PopupEntrypoint, OutputChunk, OutputFile, OutputAsset, BuildOutput, BuildStepOutput, UserManifest, Wxt, SidepanelEntrypoint, BaseEntrypoint } from '../../../types';
3
3
  type DeepPartial<T> = T extends object ? {
4
4
  [P in keyof T]?: DeepPartial<T[P]>;
5
5
  } : T;
6
6
  export declare function fakeFileName(): string;
7
7
  export declare function fakeFile(root?: string): string;
8
8
  export declare function fakeDir(root?: string): string;
9
- export declare const fakeEntrypoint: () => GenericEntrypoint | BackgroundEntrypoint | ContentScriptEntrypoint | PopupEntrypoint | OptionsEntrypoint;
9
+ export declare const fakeEntrypoint: (options?: DeepPartial<BaseEntrypoint>) => GenericEntrypoint | BackgroundEntrypoint | ContentScriptEntrypoint | PopupEntrypoint | OptionsEntrypoint;
10
10
  export declare const fakeContentScriptEntrypoint: (overrides?: {
11
11
  type?: "content-script" | undefined;
12
12
  options?: {
@@ -8104,6 +8104,7 @@ export declare const fakeWxt: (overrides?: {
8104
8104
  name?: string | undefined;
8105
8105
  version?: string | undefined;
8106
8106
  importEntrypoint?: {} | undefined;
8107
+ importEntrypoints?: {} | undefined;
8107
8108
  build?: {} | undefined;
8108
8109
  createServer?: {} | undefined;
8109
8110
  } | undefined;
@@ -17,14 +17,14 @@ export function fakeFile(root = process.cwd()) {
17
17
  export function fakeDir(root = process.cwd()) {
18
18
  return resolve(root, faker.string.alphanumeric());
19
19
  }
20
- export const fakeEntrypoint = () => faker.helpers.arrayElement([
20
+ export const fakeEntrypoint = (options) => faker.helpers.arrayElement([
21
21
  fakePopupEntrypoint,
22
22
  fakeGenericEntrypoint,
23
23
  fakeOptionsEntrypoint,
24
24
  fakeBackgroundEntrypoint,
25
25
  fakeContentScriptEntrypoint,
26
26
  fakeUnlistedScriptEntrypoint
27
- ])();
27
+ ])(options);
28
28
  export const fakeContentScriptEntrypoint = fakeObjectCreator(() => ({
29
29
  type: "content-script",
30
30
  inputPath: fakeFile("src"),
package/dist/types.d.ts CHANGED
@@ -927,9 +927,13 @@ export interface WxtBuilder {
927
927
  */
928
928
  version: string;
929
929
  /**
930
- * Import the entrypoint file, returning the default export containing the options.
930
+ * Import a JS entrypoint file, returning the default export containing the options.
931
931
  */
932
932
  importEntrypoint<T>(path: string): Promise<T>;
933
+ /**
934
+ * Import a list of JS entrypoint files, returning their options.
935
+ */
936
+ importEntrypoints(paths: string[]): Promise<Record<string, unknown>[]>;
933
937
  /**
934
938
  * Build a single entrypoint group. This is effectively one of the multiple "steps" during the
935
939
  * build process.
@@ -1288,6 +1292,8 @@ export interface FsCache {
1288
1292
  export interface ExtensionRunner {
1289
1293
  openBrowser(): Promise<void>;
1290
1294
  closeBrowser(): Promise<void>;
1295
+ /** Whether or not this runner actually opens the browser. */
1296
+ canOpen?(): boolean;
1291
1297
  }
1292
1298
  export type EslintGlobalsPropValue = boolean | 'readonly' | 'readable' | 'writable' | 'writeable';
1293
1299
  export interface Eslintrc {
package/dist/version.mjs CHANGED
@@ -1 +1 @@
1
- export const version = "0.19.19";
1
+ export const version = "0.19.21";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wxt",
3
3
  "type": "module",
4
- "version": "0.19.19",
4
+ "version": "0.19.21",
5
5
  "description": "Next gen framework for developing web extensions",
6
6
  "repository": {
7
7
  "type": "git",
@@ -70,6 +70,7 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
+ "@1natsu/wait-element": "^4.1.2",
73
74
  "@aklinker1/rollup-plugin-visualizer": "5.12.0",
74
75
  "@types/chrome": "^0.0.280",
75
76
  "@types/webextension-polyfill": "^0.12.1",