@crxjs/vite-plugin 2.0.0-beta.9 → 2.0.0

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.
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ > [!IMPORTANT]
2
+ > CRXJS is seeking new maintainers. If no maintenance team is established by March 31, 2025, this repository will be archived. [Learn more](https://github.com/crxjs/chrome-extension-tools/discussions/974).
3
+
1
4
  # ![CRXJS](./banner-github.png)
2
5
 
3
6
  [![npm (scoped)](https://img.shields.io/npm/v/@crxjs/vite-plugin.svg)](https://www.npmjs.com/package/@crxjs/vite-plugin)
package/client.d.ts CHANGED
@@ -57,3 +57,18 @@ declare module '*?script&module' {
57
57
  const fileName: string
58
58
  export default fileName
59
59
  }
60
+
61
+ declare namespace ContentScriptAPI {
62
+ export interface ExecuteFnOptions {
63
+ perf: {
64
+ injectTime: number
65
+ loadTime: number
66
+ }
67
+ }
68
+ export interface ExecuteFn {
69
+ (options: ExecuteFnOptions): void
70
+ }
71
+ export interface ModuleExports {
72
+ onExecute?: ExecuteFn
73
+ }
74
+ }
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  const defineManifest = (manifest) => manifest;
6
6
  const defineDynamicResource = ({
7
7
  matches = ["http://*/*", "https://*/*"],
8
- use_dynamic_url = true
8
+ use_dynamic_url = false
9
9
  }) => ({
10
10
  matches,
11
11
  resources: [DYNAMIC_RESOURCE],
package/dist/index.d.ts CHANGED
@@ -17,6 +17,14 @@ interface WebAccessibleResourceById {
17
17
  resources: string[];
18
18
  use_dynamic_url?: boolean;
19
19
  }
20
+ interface ChromeManifestBackground {
21
+ service_worker: string;
22
+ type?: 'module' | (string & {});
23
+ }
24
+ interface FirefoxManifestBackground {
25
+ scripts: string[];
26
+ persistent?: false;
27
+ }
20
28
  interface ManifestV3 {
21
29
  manifest_version: number;
22
30
  name: string;
@@ -25,11 +33,13 @@ interface ManifestV3 {
25
33
  description?: string | undefined;
26
34
  icons?: chrome.runtime.ManifestIcons | undefined;
27
35
  action?: chrome.runtime.ManifestAction | undefined;
28
- author?: string | undefined;
29
- background?: {
30
- service_worker: string;
31
- type?: 'module';
36
+ /**
37
+ * @see https://developer.chrome.com/docs/extensions/reference/manifest/author
38
+ */
39
+ author?: {
40
+ email: string;
32
41
  } | undefined;
42
+ background?: ChromeManifestBackground | FirefoxManifestBackground | undefined;
33
43
  chrome_settings_overrides?: {
34
44
  homepage?: string | undefined;
35
45
  search_provider?: chrome.runtime.SearchProvider | undefined;
@@ -118,12 +128,12 @@ interface ManifestV3 {
118
128
  } | undefined;
119
129
  incognito?: string | undefined;
120
130
  input_components?: {
121
- name?: string | undefined;
122
- type?: string | undefined;
131
+ name: string;
123
132
  id?: string | undefined;
124
- description?: string | undefined;
125
- language?: string | undefined;
126
- layouts?: string[] | undefined;
133
+ language?: string | string[] | undefined;
134
+ layouts?: string | string[] | undefined;
135
+ input_view?: string | undefined;
136
+ options_page?: string | undefined;
127
137
  }[] | undefined;
128
138
  key?: string | undefined;
129
139
  minimum_chrome_version?: string | undefined;
@@ -166,6 +176,9 @@ interface ManifestV3 {
166
176
  pages: string[];
167
177
  content_security_policy?: string | undefined;
168
178
  } | undefined;
179
+ side_panel?: {
180
+ default_path?: string | undefined;
181
+ } | undefined;
169
182
  short_name?: string | undefined;
170
183
  spellcheck?: {
171
184
  dictionary_language?: string | undefined;
@@ -189,9 +202,98 @@ interface ManifestV3 {
189
202
  web_accessible_resources?: (WebAccessibleResourceById | WebAccessibleResourceByMatch)[] | undefined;
190
203
  }
191
204
 
192
- declare type ManifestV3Export = ManifestV3 | Promise<ManifestV3> | ManifestV3Fn;
193
- declare type ManifestV3Fn = (env: ConfigEnv) => ManifestV3 | Promise<ManifestV3>;
194
- declare const defineManifest: (manifest: ManifestV3Export) => ManifestV3Export;
205
+ type ManifestV3Fn = (env: ConfigEnv) => ManifestV3 | Promise<ManifestV3>;
206
+ type ManifestV3Export = ManifestV3 | Promise<ManifestV3> | ManifestV3Fn;
207
+ type Code = '.' | '/' | '\\';
208
+ type ManifestFilePath<T extends string> = T extends `${Code}${string}` ? never : T extends `${string}.${infer Ext}` ? Ext extends '' ? never : T : never;
209
+ interface ManifestIcons<T extends string> {
210
+ [size: number]: ManifestFilePath<T>;
211
+ }
212
+ type FilePathFields<T extends string> = {
213
+ icons?: ManifestIcons<T>;
214
+ action?: {
215
+ /**
216
+ * - Relative to Vite project root (where vite.config.js is)
217
+ * - Format: "subdir/icon.png" (no leading ./ or /)
218
+ *
219
+ * @example "assets/icon.png"
220
+ */
221
+ default_icon?: ManifestIcons<T>;
222
+ default_title?: string;
223
+ /**
224
+ * - Relative to Vite project root (where vite.config.js is)
225
+ * - Format: "subdir/index.html" (no leading ./ or /)
226
+ *
227
+ * @example "src/popup.html"
228
+ */
229
+ default_popup?: ManifestFilePath<T>;
230
+ };
231
+ background?: {
232
+ /**
233
+ * - Relative to Vite project root (where vite.config.js is)
234
+ * - Format: "subdir/index.js" (no leading ./ or /)
235
+ *
236
+ * @example "src/background.js"
237
+ */
238
+ service_worker: ManifestFilePath<T>;
239
+ type?: 'module' | (string & {});
240
+ } | FirefoxManifestBackground;
241
+ content_scripts?: {
242
+ matches?: string[];
243
+ exclude_matches?: string[];
244
+ /**
245
+ * - Relative to Vite project root (where vite.config.js is)
246
+ * - Format: "subdir/content.css" (no leading ./ or /)
247
+ *
248
+ * @example "src/content.css"
249
+ */
250
+ css?: ManifestFilePath<T>[];
251
+ /**
252
+ * - Relative to Vite project root (where vite.config.js is)
253
+ * - Format: "subdir/content.js" (no leading ./ or /)
254
+ *
255
+ * @example "src/content.js"
256
+ */
257
+ js?: ManifestFilePath<T>[];
258
+ run_at?: string;
259
+ all_frames?: boolean;
260
+ match_about_blank?: boolean;
261
+ include_globs?: string[];
262
+ exclude_globs?: string[];
263
+ }[];
264
+ input_components?: {
265
+ name: string;
266
+ id?: string;
267
+ language?: string | string[];
268
+ layouts?: string | string[];
269
+ input_view?: string;
270
+ /**
271
+ * - Relative to Vite project root (where vite.config.js is)
272
+ * - Format: "subdir/options.html" (no leading ./ or /)
273
+ *
274
+ * @example "src/options.html"
275
+ */
276
+ options_page?: ManifestFilePath<T>;
277
+ }[];
278
+ /**
279
+ * - Relative to Vite project root (where vite.config.js is)
280
+ * - Format: "subdir/options.html" (no leading ./ or /)
281
+ *
282
+ * @example "src/options.html"
283
+ */
284
+ options_page?: ManifestFilePath<T>;
285
+ /**
286
+ * - Relative to Vite project root (where vite.config.js is)
287
+ * - Format: "subdir/devtools.html" (no leading ./ or /)
288
+ *
289
+ * @example "src/devtools.html"
290
+ */
291
+ devtools_page?: ManifestFilePath<T>;
292
+ };
293
+ type ManifestOptions<T extends string> = Omit<ManifestV3, keyof FilePathFields<string>> & FilePathFields<T>;
294
+ type ManifestV3Options<T extends string = string> = ManifestOptions<T> | Promise<ManifestOptions<T>> | ManifestV3Define<T>;
295
+ type ManifestV3Define<T extends string> = (env: ConfigEnv) => ManifestOptions<T> | Promise<ManifestOptions<T>>;
296
+ declare const defineManifest: <T extends string>(manifest: ManifestV3Options<T>) => ManifestV3Export;
195
297
  /**
196
298
  * Content script resources like CSS and image files must be declared in the
197
299
  * manifest under `web_accessible_resources`. Manifest V3 uses a match pattern
@@ -219,19 +321,25 @@ declare const defineManifest: (manifest: ManifestV3Export) => ManifestV3Export;
219
321
  */
220
322
  declare const defineDynamicResource: ({ matches, use_dynamic_url, }: Omit<WebAccessibleResourceByMatch, 'resources'>) => WebAccessibleResourceByMatch;
221
323
 
222
- declare type CrxDevAssetId = {
324
+ type CrxDevAssetId = {
223
325
  id: string;
224
326
  type: 'asset';
225
327
  source?: string | Uint8Array;
226
328
  };
227
- declare type CrxDevScriptId = {
329
+ type CrxDevScriptId = {
228
330
  id: string;
229
331
  type: 'module' | 'iife';
230
332
  };
231
333
  interface CrxPlugin extends Plugin {
232
- /** Runs during the transform hook for the manifest. Filenames use input filenames. */
334
+ /**
335
+ * Runs during the transform hook for the manifest. Filenames use input
336
+ * filenames.
337
+ */
233
338
  transformCrxManifest?: (this: PluginContext, manifest: ManifestV3) => Promise<ManifestV3 | null | undefined> | ManifestV3 | null | undefined;
234
- /** Runs during generateBundle, before manifest output. Filenames use output filenames. */
339
+ /**
340
+ * Runs during generateBundle, before manifest output. Filenames use output
341
+ * filenames.
342
+ */
235
343
  renderCrxManifest?: (this: PluginContext, manifest: ManifestV3, bundle: OutputBundle) => Promise<ManifestV3 | null | undefined> | ManifestV3 | null | undefined;
236
344
  /**
237
345
  * Runs in the file writer on content scripts during development. `script.id`
@@ -246,12 +354,18 @@ interface CrxOptions {
246
354
  injectCss?: boolean;
247
355
  };
248
356
  fastGlobOptions?: Options;
357
+ /**
358
+ * The browser that this extension is targeting, can be "firefox" or "chrome".
359
+ * Default is "chrome".
360
+ */
361
+ browser?: Browser;
249
362
  }
363
+ type Browser = 'firefox' | 'chrome';
250
364
 
251
365
  /** Resolves when all existing files in scriptFiles are written. */
252
366
  declare function allFilesReady(): Promise<void>;
253
367
 
254
- declare type FileWriterId = {
368
+ type FileWriterId = {
255
369
  type: CrxDevAssetId['type'] | CrxDevScriptId['type'] | 'loader';
256
370
  id: string;
257
371
  };
package/dist/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  import { simple } from 'acorn-walk';
2
2
  import { createHash } from 'crypto';
3
- import debug$2 from 'debug';
4
- import v8 from 'v8';
5
- import { posix } from 'path';
6
- import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
3
+ import debug$3 from 'debug';
4
+ import { join, normalize, isAbsolute, basename, relative, resolve, dirname, parse } from 'pathe';
5
+ import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, BehaviorSubject, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
7
6
  import fsx from 'fs-extra';
8
7
  import { performance } from 'perf_hooks';
9
8
  import { rollup } from 'rollup';
10
9
  import * as lexer from 'es-module-lexer';
11
10
  import { readFile as readFile$1 } from 'fs/promises';
12
11
  import MagicString from 'magic-string';
13
- import { createLogger } from 'vite';
12
+ import convertSourceMap from 'convert-source-map';
13
+ import { createLogger, version } from 'vite';
14
14
  import { readFileSync, existsSync, promises } from 'fs';
15
15
  import { createRequire } from 'module';
16
16
  import fg from 'fast-glob';
@@ -24,8 +24,10 @@ const pluginOptionsProvider = (options) => {
24
24
  name: pluginName$1,
25
25
  api: {
26
26
  crx: {
27
+ // during testing this can be null, we don't provide options through the test config
27
28
  options
28
29
  }
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
31
  }
30
32
  };
31
33
  };
@@ -56,12 +58,9 @@ function isCrxPlugin(p) {
56
58
  return !!p && typeof p === "object" && !(p instanceof Promise) && !Array.isArray(p) && p.name.startsWith("crx:");
57
59
  }
58
60
 
59
- var workerHmrClient = "const ownOrigin = new URL(chrome.runtime.getURL(\"/\")).origin;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(url));\n }\n});\nasync function sendToServer(url) {\n url.protocol = \"http:\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"));\n return new Response(response.body, {\n headers: {\n \"Content-Type\": response.headers.get(\"Content-Type\") ?? \"text/javascript\"\n }\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => ports.delete(port2));\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(`${socketProtocol}://${socketHost}`, \"vite-hmr\");\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n});\n";
61
+ var workerHmrClient = "const ownOrigin = `chrome-extension://${chrome.runtime.id}`;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(fetchEvent.request));\n }\n});\nasync function sendToServer(req) {\n const url = new URL(req.url);\n const requestHeaders = new Headers(req.headers);\n url.protocol = \"http:\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"), {\n headers: requestHeaders\n });\n const responseHeaders = new Headers(response.headers);\n responseHeaders.set(\"Content-Type\", responseHeaders.get(\"Content-Type\") ?? \"text/javascript\");\n responseHeaders.set(\"Cache-Control\", responseHeaders.get(\"Cache-Control\") ?? \"\");\n return new Response(response.body, {\n headers: responseHeaders\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => {\n if (chrome.runtime.lastError) {\n console.error(chrome.runtime.lastError);\n }\n ports.delete(port2);\n });\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketToken = __HMR_TOKEN__;\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(`${socketProtocol}://${socketHost}?token=${socketToken}`, \"vite-hmr\");\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n});\n";
60
62
 
61
- const _debug = (id) => debug$2("crx").extend(id);
62
- const structuredClone = (obj) => {
63
- return v8.deserialize(v8.serialize(obj));
64
- };
63
+ const _debug = (id) => debug$3("crx").extend(id);
65
64
  const hash = (data, length = 5) => createHash("sha1").update(data).digest("base64").replace(/[^A-Za-z0-9]/g, "").slice(0, length);
66
65
  const isString = (x) => typeof x === "string";
67
66
  function isObject(value) {
@@ -113,22 +112,6 @@ const getMatchPatternOrigin = (pattern) => {
113
112
  return pathname ? `${root}/*` : root;
114
113
  };
115
114
 
116
- const {
117
- basename,
118
- dirname,
119
- extname,
120
- delimiter,
121
- format,
122
- isAbsolute,
123
- join,
124
- normalize,
125
- parse,
126
- relative,
127
- resolve,
128
- toNamespacedPath,
129
- sep
130
- } = posix;
131
-
132
115
  function defineClientValues(code, config) {
133
116
  let options = config.server.hmr;
134
117
  options = options && typeof options !== "boolean" ? options : {};
@@ -152,7 +135,7 @@ function defineClientValues(code, config) {
152
135
  if (hmrBase !== "/") {
153
136
  hmrPort = normalize(`${hmrPort}${hmrBase}`);
154
137
  }
155
- return code.replace(`__MODE__`, JSON.stringify(config.mode)).replace(`__BASE__`, JSON.stringify(config.base)).replace(`__DEFINES__`, serializeDefine(config.define || {})).replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)).replace(`__HMR_HOSTNAME__`, JSON.stringify(host)).replace(`__HMR_PORT__`, JSON.stringify(hmrPort)).replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout)).replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay)).replace(`__SERVER_PORT__`, JSON.stringify(config.server.port?.toString()));
138
+ return code.replace(`__MODE__`, JSON.stringify(config.mode)).replace(`__BASE__`, JSON.stringify(config.base)).replace(`__DEFINES__`, serializeDefine(config.define || {})).replace(`__HMR_TOKEN__`, JSON.stringify(config.webSocketToken || "")).replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)).replace(`__HMR_HOSTNAME__`, JSON.stringify(host)).replace(`__HMR_PORT__`, JSON.stringify(hmrPort)).replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout)).replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay)).replace(`__SERVER_PORT__`, JSON.stringify(config.server.port?.toString()));
156
139
  function serializeDefine(define) {
157
140
  let res = `{`;
158
141
  for (const key in define) {
@@ -204,7 +187,7 @@ function formatFileData(script) {
204
187
  return script;
205
188
  }
206
189
  function getFileName({ type, id }) {
207
- let fileName = id.replace(/t=\d+&/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
190
+ let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
208
191
  if (fileName.includes("node_modules/")) {
209
192
  fileName = `vendor/${fileName.split("node_modules/").pop().replace(/\//g, "-")}`;
210
193
  } else if (fileName.startsWith("@")) {
@@ -272,6 +255,7 @@ const workerClientId = "/@crx/client-worker";
272
255
 
273
256
  const pluginBackground = () => {
274
257
  let config;
258
+ let browser;
275
259
  return [
276
260
  {
277
261
  name: "crx:background-client",
@@ -292,24 +276,39 @@ const pluginBackground = () => {
292
276
  },
293
277
  {
294
278
  name: "crx:background-loader-file",
279
+ // this should happen after other plugins; the loader file is an implementation detail
295
280
  enforce: "post",
281
+ async config(config2) {
282
+ const opts = await getOptions(config2);
283
+ browser = opts.browser || "chrome";
284
+ },
296
285
  configResolved(_config) {
297
286
  config = _config;
298
287
  },
299
288
  renderCrxManifest(manifest) {
300
- const worker = manifest.background?.service_worker;
289
+ const worker = browser === "firefox" ? manifest.background?.scripts[0] : manifest.background?.service_worker;
301
290
  let loader;
302
291
  if (config.command === "serve") {
303
292
  const port = config.server.port?.toString();
304
293
  if (typeof port === "undefined")
305
294
  throw new Error("server port is undefined in watch mode");
306
- loader = `import 'http://localhost:${port}/@vite/env';
295
+ if (browser === "firefox") {
296
+ loader = `import('http://localhost:${port}/@vite/env');
307
297
  `;
308
- loader += `import 'http://localhost:${port}${workerClientId}';
298
+ loader += `import('http://localhost:${port}${workerClientId}');
299
+ `;
300
+ if (worker)
301
+ loader += `import('http://localhost:${port}/${worker}');
302
+ `;
303
+ } else {
304
+ loader = `import 'http://localhost:${port}/@vite/env';
309
305
  `;
310
- if (worker)
311
- loader += `import 'http://localhost:${port}/${worker}';
306
+ loader += `import 'http://localhost:${port}${workerClientId}';
312
307
  `;
308
+ if (worker)
309
+ loader += `import 'http://localhost:${port}/${worker}';
310
+ `;
311
+ }
313
312
  } else if (worker) {
314
313
  loader = `import './${worker}';
315
314
  `;
@@ -318,13 +317,21 @@ const pluginBackground = () => {
318
317
  }
319
318
  const refId = this.emitFile({
320
319
  type: "asset",
320
+ // fileName b/c service worker must be at root of crx
321
321
  fileName: getFileName({ type: "loader", id: "service-worker" }),
322
322
  source: loader
323
323
  });
324
- manifest.background = {
325
- service_worker: this.getFileName(refId),
326
- type: "module"
327
- };
324
+ if (browser !== "firefox") {
325
+ manifest.background = {
326
+ service_worker: this.getFileName(refId),
327
+ type: "module"
328
+ };
329
+ } else {
330
+ manifest.background = {
331
+ scripts: [this.getFileName(refId)],
332
+ type: "module"
333
+ };
334
+ }
328
335
  return manifest;
329
336
  }
330
337
  }
@@ -333,9 +340,9 @@ const pluginBackground = () => {
333
340
 
334
341
  var contentHmrPort = "function isCrxHMRPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nclass HMRPort {\n port;\n callbacks = /* @__PURE__ */ new Map();\n constructor() {\n setInterval(() => {\n try {\n this.port?.postMessage({ data: \"ping\" });\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"Extension context invalidated.\")) {\n location.reload();\n } else\n throw error;\n }\n }, __CRX_HMR_TIMEOUT__);\n setInterval(this.initPort, 5 * 60 * 1e3);\n this.initPort();\n }\n initPort = () => {\n this.port?.disconnect();\n this.port = chrome.runtime.connect({ name: \"@crx/client\" });\n this.port.onDisconnect.addListener(this.handleDisconnect.bind(this));\n this.port.onMessage.addListener(this.handleMessage.bind(this));\n this.port.postMessage({ type: \"connected\" });\n };\n handleDisconnect = () => {\n if (this.callbacks.has(\"close\"))\n for (const cb of this.callbacks.get(\"close\")) {\n cb({ wasClean: true });\n }\n };\n handleMessage = (message) => {\n const forward = (data) => {\n if (this.callbacks.has(\"message\"))\n for (const cb of this.callbacks.get(\"message\")) {\n cb({ data });\n }\n };\n const payload = JSON.parse(message.data);\n if (isCrxHMRPayload(payload)) {\n if (payload.event === \"crx:runtime-reload\") {\n console.log(\"[crx] runtime reload\");\n setTimeout(() => location.reload(), 500);\n } else {\n forward(JSON.stringify(payload.data));\n }\n } else {\n forward(message.data);\n }\n };\n addEventListener = (event, callback) => {\n const cbs = this.callbacks.get(event) ?? /* @__PURE__ */ new Set();\n cbs.add(callback);\n this.callbacks.set(event, cbs);\n };\n send = (data) => {\n if (this.port)\n this.port.postMessage({ data });\n else\n throw new Error(\"HMRPort is not initialized\");\n };\n}\n\nexport { HMRPort };\n";
335
342
 
336
- var contentDevLoader = "(function () {\n 'use strict';\n\n (async () => {\n if (__PREAMBLE__)\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__PREAMBLE__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__CLIENT__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n })().catch(console.error);\n\n})();\n";
343
+ var contentDevLoader = "(function () {\n 'use strict';\n\n const injectTime = performance.now();\n (async () => {\n if (__PREAMBLE__)\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__PREAMBLE__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__CLIENT__)\n );\n const { onExecute } = await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });\n })().catch(console.error);\n\n})();\n";
337
344
 
338
- var contentProLoader = "(function () {\n 'use strict';\n\n (async () => {\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n })().catch(console.error);\n\n})();\n";
345
+ var contentProLoader = "(function () {\n 'use strict';\n\n const injectTime = performance.now();\n (async () => {\n const { onExecute } = await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });\n })().catch(console.error);\n\n})();\n";
339
346
 
340
347
  const contentScripts = new RxMap();
341
348
  contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, value }) => {
@@ -393,6 +400,10 @@ const allFilesReady$ = buildEnd$.pipe(
393
400
  map(() => [...outputFiles.values()]),
394
401
  switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
395
402
  );
403
+ const timestamp$ = new BehaviorSubject(Date.now());
404
+ allFilesReady$.subscribe(() => {
405
+ timestamp$.next(Date.now());
406
+ });
396
407
  const isRejected = (x) => x?.status === "rejected";
397
408
  const fileWriterError$ = allFilesReady$.pipe(
398
409
  mergeMap((results) => results.filter(isRejected)),
@@ -426,16 +437,36 @@ function prepAsset(fileName, { id, source }) {
426
437
  }
427
438
  function prepScript(fileName, script) {
428
439
  return ($) => $.pipe(
440
+ // get script contents from dev server
429
441
  mergeMap(async ({ server }) => {
430
442
  const target = getOutputPath(server, fileName);
431
443
  const viteUrl = getViteUrl(script);
432
444
  const transformResult = await server.transformRequest(viteUrl);
433
445
  if (!transformResult)
434
446
  throw new TypeError(`Unable to load "${script.id}" from server.`);
435
- const { code, deps = [], dynamicDeps = [] } = transformResult;
436
- return { target, code, deps: [...deps, ...dynamicDeps].flat(), server };
447
+ const { deps = [], dynamicDeps = [], map: map2 } = transformResult;
448
+ let { code } = transformResult;
449
+ try {
450
+ if (map2 && server.config.build.sourcemap === "inline") {
451
+ code = code.replace(/\n*\/\/# sourceMappingURL=[^\n]+/g, "");
452
+ const sourceMap = convertSourceMap.fromObject(map2).toComment();
453
+ code += `
454
+ ${sourceMap}
455
+ `;
456
+ }
457
+ } catch (error) {
458
+ console.warn("Failed to inline source map", error);
459
+ }
460
+ return {
461
+ target,
462
+ code,
463
+ deps: [...deps, ...dynamicDeps].flat(),
464
+ server
465
+ };
437
466
  }),
467
+ // retry in case of dependency rebundle
438
468
  retry({ count: 10, delay: 100 }),
469
+ // patch content scripts
439
470
  mergeMap(async ({ target, server, ...rest }) => {
440
471
  const plugins = server.config.plugins;
441
472
  let { code, deps } = rest;
@@ -455,7 +486,8 @@ function prepScript(fileName, script) {
455
486
  if (i.n) {
456
487
  depSet.add(i.n);
457
488
  const fileName2 = getFileName({ type: "module", id: i.n });
458
- magic.overwrite(i.s, i.e, `/${fileName2}`);
489
+ const fullImport = code.substring(i.s, i.e);
490
+ magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
459
491
  }
460
492
  return { target, source: magic.toString(), deps: [...depSet] };
461
493
  })
@@ -526,8 +558,11 @@ function update(_id) {
526
558
  async function write(fileId) {
527
559
  const start2 = performance.now();
528
560
  const deps = await firstValueFrom(
561
+ // wait for start event
529
562
  start$.pipe(
563
+ // prepare either asset or script contents
530
564
  prepFileData(fileId),
565
+ // output file and add dependencies to file writer
531
566
  mergeMap(async ({ target, source, deps: deps2 }) => {
532
567
  const files = deps2.map((id) => {
533
568
  const r = [add({ id, type: "module" })];
@@ -543,6 +578,7 @@ async function write(fileId) {
543
578
  await outputFile(target, source, { encoding: "utf8" });
544
579
  return files;
545
580
  }),
581
+ // abort write operation on close event
546
582
  takeUntil(close$),
547
583
  concatWith(of([]))
548
584
  )
@@ -633,7 +669,20 @@ const pluginContentScripts = () => {
633
669
  name: "crx:content-scripts",
634
670
  apply: "build",
635
671
  enforce: "pre",
636
- generateBundle() {
672
+ config(config) {
673
+ return {
674
+ ...config,
675
+ build: {
676
+ ...config.build,
677
+ rollupOptions: {
678
+ ...config.build?.rollupOptions,
679
+ // keep exports for content script module api
680
+ preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "exports-only"
681
+ }
682
+ }
683
+ };
684
+ },
685
+ generateBundle(_options, bundle) {
637
686
  for (const [key, script] of contentScripts)
638
687
  if (key === script.refId) {
639
688
  if (script.type === "module") {
@@ -642,12 +691,22 @@ const pluginContentScripts = () => {
642
691
  } else if (script.type === "loader") {
643
692
  const fileName = this.getFileName(script.refId);
644
693
  script.fileName = fileName;
645
- const refId = this.emitFile({
646
- type: "asset",
647
- name: getFileName({ type: "loader", id: basename(script.id) }),
648
- source: createProLoader({ fileName })
649
- });
650
- script.loaderName = this.getFileName(refId);
694
+ const bundleFileInfo = bundle[fileName];
695
+ const shouldUseLoader = !(bundleFileInfo.type === "chunk" && bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
696
+ if (shouldUseLoader) {
697
+ const refId = this.emitFile({
698
+ type: "asset",
699
+ name: getFileName({
700
+ type: "loader",
701
+ id: basename(script.id)
702
+ }),
703
+ source: createProLoader({ fileName })
704
+ });
705
+ script.loaderName = this.getFileName(refId);
706
+ } else {
707
+ bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
708
+ `;
709
+ }
651
710
  } else if (script.type === "iife") {
652
711
  throw new Error("IIFE content scripts are not implemented");
653
712
  }
@@ -707,6 +766,34 @@ const pluginDynamicContentScripts = () => {
707
766
  configResolved(_config) {
708
767
  config = _config;
709
768
  },
769
+ configureServer(server) {
770
+ return () => {
771
+ server.middlewares.use(async (req, res, next) => {
772
+ try {
773
+ await allFilesReady();
774
+ next();
775
+ } catch (error) {
776
+ let err;
777
+ if (error instanceof Error) {
778
+ err = error;
779
+ } else if (typeof error === "string") {
780
+ err = new Error(error);
781
+ } else {
782
+ err = new Error(
783
+ `Unexpected error "${error}" in middleware for "${req.url}"`
784
+ );
785
+ }
786
+ server.ws.send({
787
+ type: "error",
788
+ err: {
789
+ message: err.message,
790
+ stack: err.stack ?? "no stack available"
791
+ }
792
+ });
793
+ }
794
+ });
795
+ };
796
+ },
710
797
  async resolveId(_source, importer) {
711
798
  if (importer && _source.includes("?script")) {
712
799
  const url = new URL(_source, "stub://stub");
@@ -775,7 +862,6 @@ const pluginDynamicContentScripts = () => {
775
862
  if (config.command === "build") {
776
863
  return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
777
864
  } else if (typeof script.fileName === "string") {
778
- await fileReady(script);
779
865
  return `export default ${JSON.stringify(script.fileName)};`;
780
866
  } else {
781
867
  throw new Error(
@@ -788,6 +874,12 @@ const pluginDynamicContentScripts = () => {
788
874
  {
789
875
  name: "crx:dynamic-content-scripts-build",
790
876
  apply: "build",
877
+ /**
878
+ * Replace dynamic script placeholders during build.
879
+ *
880
+ * Can't use `renderChunk` b/c pre plugin crx:content-scripts uses
881
+ * `generateBundle` to emit loaders. Must come after "enforce: pre".
882
+ */
791
883
  generateBundle(options, bundle) {
792
884
  for (const chunk of Object.values(bundle))
793
885
  if (chunk.type === "chunk") {
@@ -817,29 +909,42 @@ const pluginDynamicContentScripts = () => {
817
909
  ];
818
910
  };
819
911
 
912
+ const { remove } = fsx;
820
913
  const logger = createLogger("error", { prefix: "crxjs" });
821
914
  const pluginFileWriter = () => {
822
915
  fileWriterError$.subscribe((error) => {
823
916
  logger.error(error.err.message, { error: error.err });
824
917
  });
825
- return {
826
- name: "crx:file-writer",
827
- apply: "serve",
828
- configureServer(server) {
829
- server.httpServer?.on("listening", async () => {
830
- try {
831
- await start({ server });
832
- } catch (error) {
833
- console.error(error);
834
- server.close();
918
+ return [
919
+ {
920
+ name: "crx:file-writer-empty-out-dir",
921
+ apply: "serve",
922
+ enforce: "pre",
923
+ async configResolved(config) {
924
+ if (config.build.emptyOutDir) {
925
+ await remove(config.build.outDir);
835
926
  }
836
- });
837
- server.httpServer?.on("close", () => close());
927
+ }
838
928
  },
839
- closeBundle() {
840
- outputFiles.clear();
929
+ {
930
+ name: "crx:file-writer",
931
+ apply: "serve",
932
+ configureServer(server) {
933
+ server.httpServer?.on("listening", async () => {
934
+ try {
935
+ await start({ server });
936
+ } catch (error) {
937
+ console.error(error);
938
+ server.close();
939
+ }
940
+ });
941
+ server.httpServer?.on("close", () => close());
942
+ },
943
+ closeBundle() {
944
+ outputFiles.clear();
945
+ }
841
946
  }
842
- };
947
+ ];
843
948
  };
844
949
 
845
950
  const _require = typeof require === "undefined" ? createRequire(import.meta.url) : require;
@@ -884,7 +989,9 @@ async function manifestFiles(manifest, options = {}) {
884
989
  ) ?? [];
885
990
  const contentScripts = manifest.content_scripts?.flatMap(({ js }) => js) ?? [];
886
991
  const contentStyles = manifest.content_scripts?.flatMap(({ css }) => css);
887
- const serviceWorker = manifest.background?.service_worker;
992
+ const serviceWorker = manifest.background && "service_worker" in manifest.background ? manifest.background.service_worker : void 0;
993
+ const backgroundScripts = manifest.background && "scripts" in manifest.background ? manifest.background.scripts : void 0;
994
+ const background = serviceWorker ? [serviceWorker].filter(isString) : backgroundScripts ? backgroundScripts.filter(isString) : [];
888
995
  const htmlPages = htmlFiles(manifest);
889
996
  const icons = [
890
997
  Object.values(
@@ -905,7 +1012,7 @@ async function manifestFiles(manifest, options = {}) {
905
1012
  return r;
906
1013
  })
907
1014
  );
908
- webAccessibleResources = resources.flat().filter(isString);
1015
+ webAccessibleResources = [...new Set(resources.flat())].filter(isString);
909
1016
  }
910
1017
  return {
911
1018
  contentScripts: [...new Set(contentScripts)].filter(isString),
@@ -914,12 +1021,12 @@ async function manifestFiles(manifest, options = {}) {
914
1021
  icons: [...new Set(icons)].filter(isString),
915
1022
  locales: [...new Set(locales)].filter(isString),
916
1023
  rulesets: [...new Set(rulesets)].filter(isString),
917
- background: [serviceWorker].filter(isString),
1024
+ background,
918
1025
  webAccessibleResources
919
1026
  };
920
1027
  }
921
1028
  async function dirFiles(dir) {
922
- const files = await fg(`${dir}/**/*`);
1029
+ const files = await fg(join(dir, "**", "*"));
923
1030
  return files;
924
1031
  }
925
1032
  function htmlFiles(manifest) {
@@ -929,7 +1036,8 @@ function htmlFiles(manifest) {
929
1036
  manifest.devtools_page,
930
1037
  manifest.options_page,
931
1038
  manifest.options_ui?.page,
932
- manifest.sandbox?.pages
1039
+ manifest.sandbox?.pages,
1040
+ manifest.side_panel?.default_path
933
1041
  ].flat().filter(isString).map((s) => s.split("#")[0]).sort();
934
1042
  return [...new Set(files)];
935
1043
  }
@@ -954,11 +1062,13 @@ const pluginFileWriterPublic = () => {
954
1062
  };
955
1063
  };
956
1064
 
957
- _debug("file-writer").extend("hmr");
958
- const isCrxHMRPayload = (p) => p.type === "custom" && p.event.startsWith("crx:");
1065
+ const debug$2 = _debug("file-writer").extend("hmr");
1066
+ const isCustomPayload = (p) => {
1067
+ return p.type === "custom";
1068
+ };
959
1069
  const hmrPayload$ = new Subject();
960
1070
  const crxHMRPayload$ = hmrPayload$.pipe(
961
- filter((p) => !isCrxHMRPayload(p)),
1071
+ filter((p) => !isCustomPayload(p)),
962
1072
  buffer(allFilesReady$),
963
1073
  mergeMap((pps) => {
964
1074
  let fullReload;
@@ -1017,6 +1127,7 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1017
1127
  }
1018
1128
  }),
1019
1129
  map((data) => {
1130
+ debug$2(`hmr payload`, data);
1020
1131
  return {
1021
1132
  type: "custom",
1022
1133
  event: "crx:content-script-payload",
@@ -1056,6 +1167,7 @@ const pluginHMR = () => {
1056
1167
  name: "crx:hmr",
1057
1168
  apply: "serve",
1058
1169
  enforce: "pre",
1170
+ // server hmr host should be localhost
1059
1171
  async config({ server = {}, ...config2 }) {
1060
1172
  if (server.hmr === false)
1061
1173
  return;
@@ -1065,6 +1177,7 @@ const pluginHMR = () => {
1065
1177
  server.hmr.host = "localhost";
1066
1178
  return { server, ...config2 };
1067
1179
  },
1180
+ // server should ignore outdir
1068
1181
  configResolved(_config) {
1069
1182
  config = _config;
1070
1183
  const { watch = {} } = config.server;
@@ -1102,19 +1215,24 @@ const pluginHMR = () => {
1102
1215
  closeBundle() {
1103
1216
  subs.unsubscribe();
1104
1217
  },
1218
+ // background changes require a full extension reload
1105
1219
  handleHotUpdate({ modules, server }) {
1106
1220
  const { root } = server.config;
1107
1221
  const relFiles = /* @__PURE__ */ new Set();
1108
- for (const m of modules)
1222
+ const fsFiles = /* @__PURE__ */ new Set();
1223
+ for (const m of modules) {
1109
1224
  if (m.id?.startsWith(root)) {
1110
1225
  relFiles.add(m.id.slice(server.config.root.length));
1226
+ } else if (m.url?.startsWith("/@fs")) {
1227
+ fsFiles.add(m.url);
1111
1228
  }
1229
+ }
1230
+ fsFiles.forEach((file) => update(file));
1112
1231
  if (inputManifestFiles.background.length) {
1113
1232
  const background = prefix$1("/", inputManifestFiles.background[0]);
1114
1233
  if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
1115
1234
  debug$1("sending runtime reload");
1116
1235
  server.ws.send(crxRuntimeReload);
1117
- return [];
1118
1236
  }
1119
1237
  }
1120
1238
  for (const [key, script] of contentScripts)
@@ -1129,6 +1247,7 @@ const pluginHMR = () => {
1129
1247
  name: "crx:hmr",
1130
1248
  apply: "serve",
1131
1249
  enforce: "post",
1250
+ // get final output manifest for handleHotUpdate 👆
1132
1251
  async transformCrxManifest(manifest) {
1133
1252
  inputManifestFiles = await manifestFiles(manifest, { cwd: config.root });
1134
1253
  return null;
@@ -1219,6 +1338,7 @@ const pluginHtmlInlineScripts = () => {
1219
1338
  };
1220
1339
  const postPlugin = {
1221
1340
  name: "crx:html-auditor-post",
1341
+ // this hook isn't audited b/c we add it after we set up the auditors
1222
1342
  transformIndexHtml(html, ctx) {
1223
1343
  const key = toKey(ctx);
1224
1344
  const p = pages.get(key);
@@ -1284,9 +1404,9 @@ const pluginHtmlInlineScripts = () => {
1284
1404
  };
1285
1405
  };
1286
1406
 
1287
- var precontrollerJs = "const id = setInterval(() => location.reload(), 100);\nsetTimeout(() => clearInterval(id), 5e3);\n";
1407
+ var loadingPageScript = "const VITE_URL = \"http://localhost:%PORT%\";\ndocument.body.innerHTML = `\n<div\n id=\"app\"\n style=\"\n border: 1px solid #ddd;\n padding: 20px;\n border-radius: 5px;\n box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n \"\n >\n <h1 style=\"color: #333\">Vite Dev Mode</h1>\n <p style=\"color: #666\">\n Cannot connect to the Vite Dev Server on <a href=\"${VITE_URL}\">${VITE_URL}</a>\n </p>\n <p style=\"color: #666\">\n Double-check that Vite is working and reload the extension.\n </p>\n <p style=\"color: #666\">\n This page will close when the extension reloads.\n </p>\n <button\n style=\"\n padding: 10px 20px;\n border: none;\n background-color: #007bff;\n color: #fff;\n border-radius: 5px;\n cursor: pointer;\n \"\n >\n Reload Extension\n </button>\n </div>`;\ndocument.body.querySelector(\"button\")?.addEventListener(\"click\", () => {\n chrome.runtime.reload();\n});\nlet tries = 0;\nlet ready = false;\ndo {\n try {\n await fetch(VITE_URL);\n ready = true;\n } catch {\n const timeout = Math.min(100 * Math.pow(2, ++tries), 5e3);\n console.log(`[CRXJS] Vite Dev Server is not available on ${VITE_URL}`);\n console.log(`[CRXJS] Retrying in ${timeout}ms...`);\n await new Promise((resolve) => setTimeout(resolve, timeout));\n }\n} while (!ready);\nlocation.reload();\n";
1288
1408
 
1289
- var precontrollerHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Waiting for the extension service worker...</title>\n <script src=\"%SCRIPT%\"></script>\n </head>\n <body>\n <h1>Waiting for service worker</h1>\n\n <p>\n If you see this message, it means the service worker has not loaded fully.\n </p>\n\n <p>This page is never added in production.</p>\n </body>\n</html>\n";
1409
+ var loadingPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Vite Dev Mode</title>\n <script src=\"%SCRIPT%\" type=\"module\"></script>\n </head>\n <body\n style=\"font-family: Arial, sans-serif; padding: 20px; text-align: center\"\n >\n <h1>Vite Dev Mode</h1>\n </body>\n</html>\n";
1290
1410
 
1291
1411
  const { readFile } = promises;
1292
1412
  const pluginManifest = () => {
@@ -1465,7 +1585,7 @@ const pluginManifest = () => {
1465
1585
  })
1466
1586
  );
1467
1587
  }
1468
- if (manifest2.background?.service_worker) {
1588
+ if (manifest2.background && "service_worker" in manifest2.background) {
1469
1589
  const file = manifest2.background.service_worker;
1470
1590
  const id2 = join(config.root, file);
1471
1591
  const refId2 = this.emitFile({
@@ -1475,6 +1595,16 @@ const pluginManifest = () => {
1475
1595
  });
1476
1596
  manifest2.background.service_worker = refId2;
1477
1597
  }
1598
+ if (manifest2.background && "scripts" in manifest2.background) {
1599
+ const file = manifest2.background.scripts[0];
1600
+ const id2 = join(config.root, file);
1601
+ const refId2 = this.emitFile({
1602
+ type: "chunk",
1603
+ id: id2,
1604
+ name: basename(file)
1605
+ });
1606
+ manifest2.background.scripts = [refId2];
1607
+ }
1478
1608
  for (const file of htmlFiles(manifest2)) {
1479
1609
  const id2 = join(config.root, file);
1480
1610
  this.emitFile({
@@ -1485,7 +1615,7 @@ const pluginManifest = () => {
1485
1615
  }
1486
1616
  }
1487
1617
  const encoded = encodeManifest(manifest2);
1488
- return encoded;
1618
+ return { code: encoded, map: null };
1489
1619
  },
1490
1620
  async generateBundle(options, bundle) {
1491
1621
  const manifestName = this.getFileName(refId);
@@ -1499,11 +1629,16 @@ const pluginManifest = () => {
1499
1629
  );
1500
1630
  }
1501
1631
  } else {
1502
- if (manifest2.background?.service_worker) {
1632
+ if (manifest2.background && "service_worker" in manifest2.background) {
1503
1633
  const ref = manifest2.background.service_worker;
1504
1634
  const name = this.getFileName(ref);
1505
1635
  manifest2.background.service_worker = name;
1506
1636
  }
1637
+ if (manifest2.background && "scripts" in manifest2.background) {
1638
+ const ref = manifest2.background.scripts[0];
1639
+ const name = this.getFileName(ref);
1640
+ manifest2.background.scripts = [name];
1641
+ }
1507
1642
  manifest2.content_scripts = manifest2.content_scripts?.map(
1508
1643
  ({ js = [], ...rest }) => {
1509
1644
  return {
@@ -1563,6 +1698,7 @@ Public dir: "${config.publicDir}"`
1563
1698
  this.emitFile({
1564
1699
  type: "asset",
1565
1700
  fileName: f,
1701
+ // TODO: cache source buffer
1566
1702
  source: await readFile(filename)
1567
1703
  });
1568
1704
  }
@@ -1571,17 +1707,20 @@ Public dir: "${config.publicDir}"`
1571
1707
  if (config.command === "serve" && files.html.length) {
1572
1708
  const refId2 = this.emitFile({
1573
1709
  type: "asset",
1574
- name: "precontroller.js",
1575
- source: precontrollerJs
1710
+ name: "loading-page.js",
1711
+ source: loadingPageScript.replace(
1712
+ "%PORT%",
1713
+ `${config.server.port ?? 0}`
1714
+ )
1576
1715
  });
1577
- const precontrollerJsName = this.getFileName(refId2);
1716
+ const loadingPageScriptName = this.getFileName(refId2);
1578
1717
  files.html.map(
1579
1718
  (f) => this.emitFile({
1580
1719
  type: "asset",
1581
1720
  fileName: f,
1582
- source: precontrollerHtml.replace(
1721
+ source: loadingPageHtml.replace(
1583
1722
  "%SCRIPT%",
1584
- `/${precontrollerJsName}`
1723
+ `/${loadingPageScriptName}`
1585
1724
  )
1586
1725
  })
1587
1726
  );
@@ -1591,10 +1730,10 @@ Public dir: "${config.publicDir}"`
1591
1730
  this.emitFile({
1592
1731
  type: "asset",
1593
1732
  fileName: "manifest.json",
1594
- source: JSON.stringify(manifest2, null, 2)
1733
+ source: JSON.stringify(manifest2, null, 2) + "\n"
1595
1734
  });
1596
1735
  } else {
1597
- manifestJson.source = JSON.stringify(manifest2, null, 2);
1736
+ manifestJson.source = JSON.stringify(manifest2, null, 2) + "\n";
1598
1737
  }
1599
1738
  delete bundle[manifestName];
1600
1739
  }
@@ -1610,7 +1749,11 @@ function compileFileResources(fileName, {
1610
1749
  assets: /* @__PURE__ */ new Set(),
1611
1750
  css: /* @__PURE__ */ new Set(),
1612
1751
  imports: /* @__PURE__ */ new Set()
1613
- }) {
1752
+ }, processedFiles = /* @__PURE__ */ new Set()) {
1753
+ if (processedFiles.has(fileName)) {
1754
+ return resources;
1755
+ }
1756
+ processedFiles.add(fileName);
1614
1757
  const chunk = chunks.get(fileName);
1615
1758
  if (chunk) {
1616
1759
  const { modules, facadeModuleId, imports, dynamicImports } = chunk;
@@ -1619,7 +1762,7 @@ function compileFileResources(fileName, {
1619
1762
  for (const x of dynamicImports)
1620
1763
  resources.imports.add(x);
1621
1764
  for (const x of [...imports, ...dynamicImports])
1622
- compileFileResources(x, { chunks, files, config }, resources);
1765
+ compileFileResources(x, { chunks, files, config }, resources, processedFiles);
1623
1766
  for (const m of Object.keys(modules))
1624
1767
  if (m !== facadeModuleId) {
1625
1768
  const key = prefix$1("/", relative(config.root, m.split("?")[0]));
@@ -1632,7 +1775,8 @@ function compileFileResources(fileName, {
1632
1775
  compileFileResources(
1633
1776
  script.fileName,
1634
1777
  { chunks, files, config },
1635
- resources
1778
+ resources,
1779
+ processedFiles
1636
1780
  );
1637
1781
  }
1638
1782
  }
@@ -1651,7 +1795,7 @@ function compileFileResources(fileName, {
1651
1795
  const defineManifest = (manifest) => manifest;
1652
1796
  const defineDynamicResource = ({
1653
1797
  matches = ["http://*/*", "https://*/*"],
1654
- use_dynamic_url = true
1798
+ use_dynamic_url = false
1655
1799
  }) => ({
1656
1800
  matches,
1657
1801
  resources: [DYNAMIC_RESOURCE],
@@ -1663,22 +1807,34 @@ _debug("web-acc-res");
1663
1807
  const pluginWebAccessibleResources = () => {
1664
1808
  let config;
1665
1809
  let injectCss;
1810
+ let browser;
1666
1811
  return [
1667
1812
  {
1668
1813
  name: "crx:web-accessible-resources",
1669
1814
  apply: "serve",
1670
1815
  enforce: "post",
1816
+ async config(config2) {
1817
+ const opts = await getOptions(config2);
1818
+ browser = opts.browser || "chrome";
1819
+ },
1671
1820
  renderCrxManifest(manifest) {
1672
1821
  manifest.web_accessible_resources = manifest.web_accessible_resources ?? [];
1673
1822
  manifest.web_accessible_resources = manifest.web_accessible_resources.map(({ resources, ...rest }) => ({
1674
1823
  resources: resources.filter((r) => r !== DYNAMIC_RESOURCE),
1675
1824
  ...rest
1676
1825
  })).filter(({ resources }) => resources.length);
1677
- manifest.web_accessible_resources.push({
1678
- use_dynamic_url: true,
1826
+ const war = {
1827
+ // all web origins can access
1679
1828
  matches: ["<all_urls>"],
1680
- resources: ["**/*", "*"]
1681
- });
1829
+ // all resources are web accessible
1830
+ resources: ["**/*", "*"],
1831
+ // change the extension origin on every reload
1832
+ use_dynamic_url: false
1833
+ };
1834
+ if (browser === "firefox") {
1835
+ delete war.use_dynamic_url;
1836
+ }
1837
+ manifest.web_accessible_resources.push(war);
1682
1838
  return manifest;
1683
1839
  }
1684
1840
  },
@@ -1687,7 +1843,9 @@ const pluginWebAccessibleResources = () => {
1687
1843
  apply: "build",
1688
1844
  enforce: "post",
1689
1845
  async config({ build, ...config2 }, { command }) {
1690
- const { contentScripts: contentScripts2 = {} } = await getOptions(config2);
1846
+ const opts = await getOptions(config2);
1847
+ const contentScripts2 = opts.contentScripts || {};
1848
+ browser = opts.browser || "chrome";
1691
1849
  injectCss = contentScripts2.injectCss ?? true;
1692
1850
  return { ...config2, build: { ...build, manifest: command === "build" } };
1693
1851
  },
@@ -1716,9 +1874,11 @@ const pluginWebAccessibleResources = () => {
1716
1874
  dynamicScriptMatches.add("https://*/*");
1717
1875
  }
1718
1876
  if (contentScripts.size > 0) {
1877
+ const viteMajorVersion = parseInt(version.split(".")[0]);
1878
+ const manifestPath = viteMajorVersion > 4 ? ".vite/manifest.json" : "manifest.json";
1719
1879
  const viteManifest = parseJsonAsset(
1720
1880
  bundle,
1721
- "manifest.json"
1881
+ manifestPath
1722
1882
  );
1723
1883
  const viteFiles = /* @__PURE__ */ new Map();
1724
1884
  for (const [, file] of Object.entries(viteManifest))
@@ -1746,12 +1906,12 @@ const pluginWebAccessibleResources = () => {
1746
1906
  { chunks: bundleChunks, files: viteFiles, config }
1747
1907
  );
1748
1908
  contentScripts.get(key).css = [...css];
1749
- if (type === "loader")
1909
+ if (type === "loader" || isDynamicScript)
1750
1910
  imports.add(fileName);
1751
1911
  const resource = {
1752
1912
  matches: isDynamicScript ? [...dynamicScriptMatches] : matches,
1753
1913
  resources: [...assets, ...imports],
1754
- use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : true
1914
+ use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : false
1755
1915
  };
1756
1916
  if (isDynamicScript || !injectCss) {
1757
1917
  resource.resources.push(...css);
@@ -1795,6 +1955,11 @@ const pluginWebAccessibleResources = () => {
1795
1955
  use_dynamic_url
1796
1956
  });
1797
1957
  }
1958
+ if (browser === "firefox") {
1959
+ for (const war of combinedResources) {
1960
+ delete war.use_dynamic_url;
1961
+ }
1962
+ }
1798
1963
  if (combinedResources.length === 0)
1799
1964
  delete manifest.web_accessible_resources;
1800
1965
  else
@@ -1806,6 +1971,7 @@ const pluginWebAccessibleResources = () => {
1806
1971
  };
1807
1972
 
1808
1973
  const crx = (options) => {
1974
+ contentScripts.clear();
1809
1975
  return [
1810
1976
  pluginOptionsProvider(options),
1811
1977
  pluginBackground(),
package/index.cjs ADDED
@@ -0,0 +1,8 @@
1
+ Object.assign(module.exports, require('./dist/index.cjs'))
2
+
3
+ // async functions, can be redirect from ESM build
4
+ const asyncFunctions = ['crx', 'chromeExtension', 'allFilesReady', 'filesReady']
5
+ asyncFunctions.forEach((name) => {
6
+ module.exports[name] = (...args) =>
7
+ import('./dist/index.mjs').then((i) => i[name](...args))
8
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crxjs/vite-plugin",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.0",
4
4
  "description": "Build Chrome Extensions with this Vite plugin.",
5
5
  "keywords": [
6
6
  "rollup-plugin",
@@ -35,60 +35,45 @@
35
35
  "types": "./client.d.ts"
36
36
  }
37
37
  },
38
- "main": "dist/index.cjs",
38
+ "main": "index.cjs",
39
39
  "module": "dist/index.mjs",
40
40
  "types": "dist/index.d.ts",
41
41
  "files": [
42
42
  "dist",
43
43
  "types",
44
44
  "manifest.schema.json",
45
- "schema",
45
+ "index.cjs",
46
46
  "client.d.ts"
47
47
  ],
48
- "scripts": {
49
- "format": "prettier -w -c ../../.prettierrc.yaml",
50
- "build": "run-s build:clean build:js",
51
- "build:clean": "rimraf dist",
52
- "build:js": "rollup -c rollup.config.ts --configPlugin esbuild",
53
- "dev:js": "npm run build:js -- -w",
54
- "dev:lint": "tsc --noEmit --watch",
55
- "lint": "run-s lint:eslint lint:types",
56
- "lint:eslint": "eslint \"{src,test}/**/*.ts\"",
57
- "lint:types": "tsc --noEmit",
58
- "test": "vitest --mode unit",
59
- "test:e2e": "vitest --mode e2e",
60
- "test:run": "run-s test:run:*",
61
- "test:run:out": "vitest --run --mode out",
62
- "test:run:e2e": "vitest --run --mode e2e",
63
- "test:update": "run-s \"test:run:out --update\" \"test:run:e2e --update\""
64
- },
65
48
  "dependencies": {
66
49
  "@rollup/pluginutils": "^4.1.2",
67
50
  "@webcomponents/custom-elements": "^1.5.0",
68
51
  "acorn-walk": "^8.2.0",
69
52
  "cheerio": "^1.0.0-rc.10",
70
- "connect-injector": "^0.4.4",
53
+ "convert-source-map": "^1.7.0",
71
54
  "debug": "^4.3.3",
72
55
  "es-module-lexer": "^0.10.0",
73
56
  "fast-glob": "^3.2.11",
74
57
  "fs-extra": "^10.0.1",
75
58
  "jsesc": "^3.0.2",
76
- "magic-string": "^0.26.0",
59
+ "magic-string": "^0.30.12",
60
+ "pathe": "^2.0.1",
77
61
  "picocolors": "^1.0.0",
78
62
  "react-refresh": "^0.13.0",
79
- "rollup": "2.78.1",
63
+ "rollup": "2.79.2",
80
64
  "rxjs": "7.5.7"
81
65
  },
82
66
  "devDependencies": {
83
67
  "@extend-chrome/messages": "1.2.2",
84
68
  "@extend-chrome/storage": "1.5.0",
85
- "@rollup/plugin-alias": "4.0.2",
69
+ "@rollup/plugin-alias": "4.0.4",
86
70
  "@rollup/plugin-commonjs": "21.1.0",
87
71
  "@rollup/plugin-json": "^5.0.0",
88
72
  "@rollup/plugin-node-resolve": "13.2.0",
89
- "@sveltejs/vite-plugin-svelte": "1.1.0",
73
+ "@sveltejs/vite-plugin-svelte": "1.4.0",
90
74
  "@types/acorn": "4.0.6",
91
- "@types/chrome": "0.0.198",
75
+ "@types/chrome": "0.0.237",
76
+ "@types/convert-source-map": "^2.0.0",
92
77
  "@types/debug": "4.1.7",
93
78
  "@types/fs-extra": "9.0.13",
94
79
  "@types/jest-image-snapshot": "^5.1.0",
@@ -98,26 +83,43 @@
98
83
  "@types/react-dom": "17.0.18",
99
84
  "@typescript-eslint/eslint-plugin": "5.41.0",
100
85
  "@typescript-eslint/parser": "5.41.0",
101
- "@vitejs/plugin-react": "^2.1.0",
86
+ "@vitejs/plugin-react": "^2.2.0",
102
87
  "@vitejs/plugin-vue": "3.2.0",
103
88
  "chokidar": "^3.5.3",
104
- "esbuild": "0.15.16",
89
+ "esbuild": "0.17.14",
105
90
  "esbuild-runner": "2.2.2",
106
- "eslint": "8.26.0",
91
+ "eslint": "8.43.0",
107
92
  "eslint-plugin-react": "^7.29.4",
108
93
  "jest-image-snapshot": "^5.2.0",
109
94
  "npm-run-all": "^4.1.5",
110
- "playwright-chromium": "1.27.1",
95
+ "playwright-chromium": "1.33.0",
111
96
  "react": "17.0.2",
112
97
  "react-dom": "17.0.2",
113
98
  "rimraf": "3.0.2",
114
99
  "rollup-plugin-dts": "^4.2.0",
115
- "rollup-plugin-esbuild": "4.10.1",
100
+ "rollup-plugin-esbuild": "4.10.3",
116
101
  "svelte": "^3.48.0",
117
102
  "typescript": "^4.6.4",
118
- "vite": "^3.1.7",
119
- "vite-plugin-inspect": "0.7.5",
120
- "vitest": "0.24.3",
121
- "vue": "3.2.41"
103
+ "vite": "^3.2.11",
104
+ "vite-plugin-inspect": "0.7.25",
105
+ "vitest": "0.28.5",
106
+ "vue": "3.2.47"
107
+ },
108
+ "scripts": {
109
+ "format": "prettier -w -c ../../.prettierrc.yaml",
110
+ "build": "run-s build:clean build:js",
111
+ "build:clean": "rimraf dist",
112
+ "build:js": "rollup -c rollup.config.ts --configPlugin esbuild",
113
+ "dev:js": "npm run build:js -- -w",
114
+ "dev:lint": "tsc --noEmit --watch",
115
+ "lint": "run-s lint:eslint lint:types",
116
+ "lint:eslint": "eslint \"{src,test}/**/*.ts\"",
117
+ "lint:types": "tsc --noEmit",
118
+ "test": "vitest --mode unit",
119
+ "test:e2e": "vitest --mode e2e",
120
+ "test:run": "run-s test:run:*",
121
+ "test:run:out": "vitest --run --mode out",
122
+ "test:run:e2e": "vitest --run --mode e2e",
123
+ "test:update": "run-s \"test:run:out --update\" \"test:run:e2e --update\""
122
124
  }
123
- }
125
+ }