@addon-core/inject-css 0.3.1 → 0.4.1

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,125 +1,277 @@
1
1
  # @addon-core/inject-css
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/%40addon-core%2Finject-css.svg?logo=npm)](https://www.npmjs.com/package/@addon-core/inject-css)
4
- [![npm downloads](https://img.shields.io/npm/dm/%40addon-core%2Finject-css.svg)](https://www.npmjs.com/package/@addon-core/inject-css)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
6
- [![CI](https://github.com/addon-stack/inject-css/actions/workflows/ci.yml/badge.svg)](https://github.com/addon-stack/inject-css/actions/workflows/ci.yml)
3
+ [![npm version](https://img.shields.io/npm/v/%40addon-core%2Finject-css.svg?logo=npm&style=for-the-badge)](https://www.npmjs.com/package/@addon-core/inject-css)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40addon-core%2Finject-css.svg?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@addon-core/inject-css)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/addon-stack/inject-css/ci.yml?style=for-the-badge)](https://github.com/addon-stack/inject-css/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE.md)
7
7
 
8
- A lightweight, TypeScript-ready library for injecting CSS into browser extension pages.
9
- Automatically detects Chrome Extension Manifest V2 and V3 and delegates to the appropriate API via [@addon-core/browser](https://github.com/addon-stack/browser).
8
+ Insert and remove CSS code or extension stylesheets in browser tabs with one typed API for Manifest V2 and Manifest V3.
10
9
 
11
- ## Table of Contents
10
+ `@addon-core/inject-css` selects the correct native adapter, validates the target before delivery, and keeps unsupported browser behavior explicit.
12
11
 
13
- - [Installation](#installation)
14
- - [Usage](#usage)
15
- - [Injecting CSS Code](#injecting-css-code)
16
- - [Injecting CSS Files](#injecting-css-files)
17
- - [Updating Options](#updating-options)
18
- - [API](#api)
19
- - [Options](#options)
20
- - [Examples](#examples)
21
- - [License](#license)
12
+ - One target model for the top frame, all frames, selected frames, or selected documents
13
+ - Runtime validation that matches the TypeScript contract
14
+ - Ordered stylesheet injection
15
+ - Matching stylesheet removal where the browser exposes a native removal API
16
+ - Stable package errors for invalid, unsupported, failed, and timed-out operations
17
+ - No silent selector fallback and no extra frame-enumeration permissions
22
18
 
23
- ## Installation
24
-
25
- ### npm:
19
+ ## Install
26
20
 
27
21
  ```bash
28
22
  npm install @addon-core/inject-css
29
23
  ```
30
24
 
31
- ### pnpm:
32
-
33
25
  ```bash
34
26
  pnpm add @addon-core/inject-css
35
27
  ```
36
28
 
37
- ### yarn:
29
+ Your extension still needs the native permissions required for CSS injection, including `scripting` in MV3 and appropriate host or `activeTab` access. The package does not modify the manifest.
38
30
 
39
- ```bash
40
- yarn add @addon-core/inject-css
31
+ ## Quick start
32
+
33
+ ```ts
34
+ import injectCss from "@addon-core/inject-css";
35
+
36
+ const injector = injectCss({
37
+ target: {tabId: 123},
38
+ });
39
+
40
+ await injector.insert("body { background: #f5f5f5; }");
41
+
42
+ // The source, target, and origin match the insertion.
43
+ await injector.remove("body { background: #f5f5f5; }");
41
44
  ```
42
45
 
46
+ The package detects the current manifest version automatically. Insertion uses `tabs.insertCSS` in MV2 and `scripting.insertCSS` in MV3; removal uses the matching native removal API when available.
47
+
48
+ ## Choose what to target
49
+
50
+ Every injector has exactly one target. Selectors are mutually exclusive in TypeScript and validated again at runtime.
43
51
 
44
- ## Usage
52
+ | Need | Target |
53
+ | --- | --- |
54
+ | Main frame | `{tabId: 123}` |
55
+ | Every injectable frame | `{tabId: 123, allFrames: true}` |
56
+ | One frame | `{tabId: 123, frameIds: [7]}` |
57
+ | Selected frames | `{tabId: 123, frameIds: [0, 7, 12]}` |
58
+ | Selected documents | `{tabId: 123, documentIds: ["document-a", "document-b"]}` |
45
59
 
46
60
  ```ts
47
- import injectCss, {InjectCssOptions} from "@addon-core/inject-css";
61
+ const topFrame = injectCss({
62
+ target: {tabId: 123},
63
+ });
64
+
65
+ const selectedFrames = injectCss({
66
+ target: {tabId: 123, frameIds: [0, 7]},
67
+ });
68
+
69
+ const allFrames = injectCss({
70
+ target: {tabId: 123, allFrames: true},
71
+ });
72
+ ```
73
+
74
+ `allFrames` accepts only the literal `true`. Omitting a selector means the top frame; there is no `allFrames: false` mode.
48
75
 
49
- // Initialize an injector with a target tab ID
50
- const injector = injectCss({tabId: 123});
76
+ `documentIds` require an MV3 runtime with native document targeting. An unsupported target throws `UnsupportedInjectCssTargetError`; the package never drops `documentIds` or falls back to a broader target.
51
77
 
52
- // Inject raw CSS code into the page
53
- await injector.insert("body { background-color: #f0f0f0; }");
78
+ `allFrames` remains one native browser operation. The package does not enumerate frames or promise an exhaustive frame snapshot.
54
79
 
55
- // Inject one or more CSS files (paths relative to extension)
56
- await injector.file("styles/main.css");
57
- await injector.file(["styles/reset.css", "styles/theme.css"]);
80
+ ## Insert CSS code
58
81
 
59
- // Update options dynamically and reuse the injector
60
- injector.options({frameId: true, origin: "USER"});
61
- await injector.insert("p { color: red; }");
82
+ ```ts
83
+ await injector.insert(`
84
+ html {
85
+ color-scheme: dark;
86
+ }
87
+
88
+ body {
89
+ background: #111;
90
+ color: #eee;
91
+ }
92
+ `);
62
93
  ```
63
94
 
64
- ### Injecting CSS Code
95
+ The CSS source must be a non-empty string.
65
96
 
66
- Use the `insert(css: string)` method to inject raw CSS code.
97
+ ## Insert CSS files
67
98
 
68
- ### Injecting CSS Files
99
+ ```ts
100
+ await injector.file("styles/content.css");
69
101
 
70
- Use the `file(files: string | string[])` method to inject CSS file(s).
102
+ await injector.file([
103
+ "styles/reset.css",
104
+ "styles/theme.css",
105
+ ]);
106
+ ```
71
107
 
72
- ### Updating Options
108
+ File lists must be non-empty and every path must be a non-empty string. Files are injected in the provided order. In MV2, one file completes for the requested target before the next file starts, preserving CSS cascade order.
73
109
 
74
- Use the `options(opts: Partial<InjectCssOptions>)` method to merge or override options on an existing injector instance.
110
+ ## Remove CSS
75
111
 
76
- ## API
112
+ Remove CSS code with `remove()` and extension stylesheets with `removeFile()`:
77
113
 
78
- ### `injectCss(options: InjectCssOptions): InjectCssContract`
114
+ ```ts
115
+ await injector.remove("body { background: #f5f5f5; }");
79
116
 
80
- Creates a new CSS injector instance. Detects the manifest version (V2 or V3) via `@addon-core/browser` and delegates to the appropriate implementation.
117
+ await injector.removeFile("styles/content.css");
81
118
 
82
- #### `InjectCssContract`
119
+ await injector.removeFile([
120
+ "styles/reset.css",
121
+ "styles/theme.css",
122
+ ]);
123
+ ```
83
124
 
84
- - `insert(css: string): Promise<void>` — Inject raw CSS code.
85
- - `file(files: string | string[]): Promise<void>` — Inject one or more CSS files.
86
- - `options(opts: Partial<InjectCssOptions>): this` — Update injector options.
125
+ Removal uses the injector's current target and origin. The CSS source, file list, target, and origin must match the values used for insertion. Removing a stylesheet that is not present is a native no-op.
87
126
 
88
- ## Options
127
+ MV3 uses `scripting.removeCSS`. MV2 uses `tabs.removeCSS` only when the current browser exposes it; otherwise removal rejects with `UnsupportedInjectCssOperationError`. This capability is checked when removal is requested, so insertion remains available in MV2 browsers without `tabs.removeCSS`.
89
128
 
90
- The injector accepts the following options (passed to `injectCss(options)` and/or `injector.options(opts)`):
129
+ MV2 removes multiple files sequentially in the provided order, matching its insertion behavior. `runAt` affects insertion only because the MV2 removal API has no corresponding field.
91
130
 
92
- | Option | Type | Description |
93
- | --------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------- |
94
- | tabId | number | Required. Target browser tab ID. |
95
- | frameId | boolean \| number \| number[] | Optional. Select frames to inject into: `true` for all frames; number or array for specific. |
96
- | matchAboutBlank | boolean | Optional. (V2 only) Include `about:blank` and similar subframes. Defaults to `true`. |
97
- | runAt | 'document_start' \| 'document_end' \| 'document_idle' | Optional. (V2 only) Injection timing, matches Chrome's `runAt` in `insertCSS`. |
98
- | documentId | string \| string[] | Optional. (V3 only) Document IDs for scripting targets. |
99
- | origin | 'AUTHOR' \| 'USER' | Optional. CSS origin matching Chrome's API (`cssOrigin` in V2, `origin` in V3). |
131
+ ## Reuse an injector
100
132
 
101
- ## Examples
133
+ Replace the complete target with `target()`:
102
134
 
103
135
  ```ts
104
- import injectCss from "@addon-core/inject-css";
136
+ injector
137
+ .target({tabId: 123, frameIds: [7]})
138
+ .target({tabId: 123, allFrames: true});
139
+ ```
140
+
141
+ The second call replaces the previous selector instead of merging with it. A validation failure leaves the existing target unchanged.
142
+
143
+ Update only execution options with `options()`:
144
+
145
+ ```ts
146
+ injector.options({
147
+ origin: "USER",
148
+ timeoutMs: 8_000,
149
+ });
150
+ ```
151
+
152
+ `options()` never accepts or changes a target. Passing an explicit `undefined` resets that option instead of retaining its previous value:
105
153
 
106
- // Initialize with a mix of options
154
+ ```ts
155
+ injector.options({
156
+ origin: undefined,
157
+ timeoutMs: undefined,
158
+ });
159
+ ```
160
+
161
+ The next operation then uses the native origin default and the package's default timeout.
162
+
163
+ ## Execution options
164
+
165
+ The portable baseline is to omit adapter-specific options:
166
+
167
+ ```ts
107
168
  const injector = injectCss({
108
- tabId: 123,
109
- frameId: [1, 2], // (V2 & V3)
110
- runAt: "document_end", // (V2 only)
111
- documentId: "main-doc-id", // (V3 only)
112
- origin: "AUTHOR", // 'AUTHOR' or 'USER'
169
+ target: {tabId: 123},
170
+ origin: "AUTHOR",
171
+ timeoutMs: 5_000,
113
172
  });
173
+ ```
174
+
175
+ | Option | MV2 | MV3 |
176
+ | --- | --- | --- |
177
+ | `origin` | Mapped to `author` or `user` | Passed as `AUTHOR` or `USER` |
178
+ | `timeoutMs` | Supported; default `4_000` ms | Supported; default `4_000` ms |
179
+ | `matchAboutBlank` | Passed only when explicitly set | Rejected; no native equivalent |
180
+ | `runAt` | Passed to `tabs.insertCSS` | Rejected; no native equivalent |
181
+
182
+ When `matchAboutBlank` is omitted, the package preserves the native default instead of forcing it to `true`.
183
+
184
+ Explicit unsupported options throw `UnsupportedInjectCssOptionError`. They are never ignored silently.
185
+
186
+ ## Handle failures
187
+
188
+ ```ts
189
+ import {InjectCssBaseError} from "@addon-core/inject-css";
190
+
191
+ try {
192
+ await injector.file("styles/content.css");
193
+ } catch (error) {
194
+ if (error instanceof InjectCssBaseError) {
195
+ console.error(error.code, error.message, error.cause);
196
+ } else {
197
+ throw error;
198
+ }
199
+ }
200
+ ```
201
+
202
+ Every rejected package operation exposes an error derived from `InjectCssBaseError` with a stable `code`. Delivery and timeout errors also retain the request target and expose `operation` as `"insert"` or `"remove"`. For explicit MV2 frame targets, a delivery error may contain an `InjectCssFrameDeliveryError` cause with code `ERR_INJECT_CSS_FRAME_DELIVERY`, the failed `tabId`, `frameId`, operation, and native cause. Prefer `code` when errors may cross realms or multiple copies of the dependency may exist.
114
203
 
115
- // Inject raw CSS code
116
- await injector.insert("body { background-color: #fafafa; }");
204
+ Known validation and adapter incompatibilities fail before delivery. Browser capabilities discovered only by a native call are normalized after that call.
117
205
 
118
- // Inject one or more CSS files
119
- await injector.file(["styles/reset.css", "styles/theme.css"]);
206
+ ### What `Promise<void>` means
207
+
208
+ Native CSS injection and removal APIs do not provide a portable per-frame result. `insert()`, `file()`, `remove()`, and `removeFile()` therefore resolve with no value.
209
+
210
+ A resolved promise means the native operation completed. It does not prove that CSS was inserted or removed in every requested frame. A rejected multi-target operation is not transactional: some targets or earlier files may already have completed the requested change.
211
+
212
+ In MV2, a timeout stops the package from starting later files in a sequential insertion or removal batch. In MV3, the complete file list is handed to the browser in one native call before a timeout can occur. In either adapter, a timeout cannot cancel a native browser operation that is already in progress.
213
+
214
+ ## API reference
215
+
216
+ The factory is available as both a default and named export:
217
+
218
+ ```ts
219
+ import injectCss from "@addon-core/inject-css";
220
+ import {injectCss} from "@addon-core/inject-css";
120
221
  ```
121
222
 
223
+ ```ts
224
+ interface InjectCssContract {
225
+ insert(css: string): Promise<void>;
226
+ file(files: string | NonEmptyReadonlyArray<string>): Promise<void>;
227
+ remove(css: string): Promise<void>;
228
+ removeFile(files: string | NonEmptyReadonlyArray<string>): Promise<void>;
229
+ target(target: InjectCssTarget): this;
230
+ options(options: InjectCssExecutionOptionsPatch): this;
231
+ }
232
+ ```
233
+
234
+ Runtime exports:
235
+
236
+ ```text
237
+ injectCss
238
+ InjectCssBaseError
239
+ InjectCssDeliveryError
240
+ InjectCssFrameDeliveryError
241
+ InjectCssTimeoutError
242
+ InvalidInjectCssCodeError
243
+ InvalidInjectCssFilesError
244
+ InvalidInjectCssOptionsError
245
+ InvalidInjectCssTargetError
246
+ UnsupportedInjectCssOptionError
247
+ UnsupportedInjectCssOperationError
248
+ UnsupportedInjectCssTargetError
249
+ ```
250
+
251
+ Core type exports:
252
+
253
+ ```text
254
+ InjectCssContract
255
+ InjectCssOptions
256
+ InjectCssExecutionOptions
257
+ InjectCssExecutionOptionsPatch
258
+ InjectCssOperation
259
+ InjectCssOrigin
260
+ InjectCssTarget
261
+ InjectCssTopFrameTarget
262
+ InjectCssAllFramesTarget
263
+ InjectCssFramesTarget
264
+ InjectCssDocumentsTarget
265
+ InjectCssErrorCode
266
+ NonEmptyReadonlyArray
267
+ ```
268
+
269
+ ## Design boundaries
270
+
271
+ The package focuses on portable programmatic CSS insertion and removal. It does not enumerate frames, discover document IDs, track which stylesheets were inserted, aggregate application-specific per-frame results, or claim atomic delivery across targets.
272
+
273
+ Callers remain responsible for retaining the exact source, target, and origin needed for later removal.
122
274
 
123
275
  ## License
124
276
 
125
- MIT © Addon Stack
277
+ [MIT](LICENSE.md)