A lightweight, TypeScript-ready library for injecting JavaScript functions or external scripts into Chrome extension tabs and frames (Manifest V2 & V3).
npm install @addon-core/inject-script



A lightweight, TypeScript-ready library for injecting JavaScript functions and external script files into browser extension pages. It automatically detects Manifest V2/V3 and uses the appropriate API implementation.
``bash`
npm install @addon-core/inject-script
`bash`
pnpm add @addon-core/inject-script
`bash`
yarn add @addon-core/inject-script
`ts
import injectScript, { type InjectScriptOptions } from "@addon-core/inject-script";
// Initialize an injector for a specific tab
const injector = injectScript({
tabId: 123,
frameId: false, // top frame only
matchAboutBlank: true, // include about:blank and similar pages
runAt: "document_idle", // injection timing (MV2)
// timeFallback: 5000, // (MV2) default timeout is 4000 ms
// world: 'ISOLATED', // (MV3) execution world
// documentId: 'abc123', // (MV3) target by documentId
} satisfies InjectScriptOptions);
// Execute a function in the page context (for all target frames)
const results = await injector.run(
(msg: string) => {
console.log(msg);
return Echo: ${msg};
},
["Hello from the extension!"]
);
// Inject one or more external files
await injector.file("scripts/content.js");
await injector.file(["scripts/lib.js", "scripts/util.js"]);
`
- Unified API for Manifest V2 and V3 (version detection via @addon-core/browser).run
- Inject functions () and files (file).frameId[]
- Precise targeting: top frame, specific , all frames, or (MV3) documentId[].world
- support (MV3): MAIN/ISOLATED; instant injection when runAt: 'document_start'.InjectionResult
- Strongly-typed results: returns an array of (one per frame).options()
- Update options on the fly with .
Creates and returns a new script injector. The implementation is chosen internally based on your manifest (MV2/MV3).
#### Contract
`ts
interface InjectScriptContract {
run(
func: (...args: A) => R,
args?: A
): Promise
file(files: string | string[]): Promise
options(options: Partial
}
`
#### Options
`ts
interface InjectScriptOptions {
tabId: number;
frameId?: boolean | number | number[];
matchAboutBlank?: boolean; // defaults to true (MV2/MV3)
// MV2
runAt?: chrome.extensionTypes.RunAt; // 'document_start' | 'document_end' | 'document_idle'
timeFallback?: number; // timeout in ms, default 4000
// MV3
world?: chrome.scripting.ExecutionWorld | ${chrome.scripting.ExecutionWorld}; // 'MAIN' | 'ISOLATED'`
documentId?: string | string[]; // Firefox does not support documentIds in target
}
- frameId: true — all frames; a number or array — specific frameIds; false or undefined — top frame only.matchAboutBlank
- : if omitted, the library enables it by default (true).runAt
- : Chrome default is document_idle (when not specified).timeFallback
- (MV2): if results do not arrive in time, the promise will be rejected with an error.world
- (MV3): sets the execution world. When runAt: 'document_start', injectImmediately is enabled.documentId
- (MV3): target specific documents. Firefox does not support documentIds; the library gracefully avoids using them there.
Inject into specific frames:
`ts`
const injector = injectScript({ tabId: 123, frameId: [0, 2] });
const results = await injector.run(() => window.location.href);
console.log(results.map(r => ({ frameId: r.frameId, url: r.result })));
All frames:
`ts`
await injectScript({ tabId: 123, frameId: true }).file(["a.js", "b.js"]);
MV3: target by documentId and choose execution world:
`ts`
await injectScript({
tabId: 123,
documentId: ["doc-1", "doc-2"],
world: "MAIN",
}).run(() => ({ ready: document.readyState }));
Update options on the fly:
`ts`
const inj = injectScript({ tabId: 123, frameId: false });
await inj.options({ frameId: true }).file("content.js");
- MV2: the library serializes your function and arguments, executes the code in target frames, and returns results via chrome.runtime.sendMessage.undefined
- If your function throws, the result for that frame will be , and the error will be logged in the page DevTools console.timeFallback
- Timeout is controlled by the option (default 4000 ms).frameId = 0
- Result order: the top frame () is the first element in the array.chrome.scripting.executeScript
- MV3: uses with a properly constructed target (tabId, frameIds/allFrames, or documentIds when available) and world/injectImmediately options.documentIds
- Firefox does not support — the library will automatically avoid using them.
- Inject as early as possible:
- Set runAt: 'document_start' (MV2); in MV3 this enables injectImmediately: true.world: 'ISOLATED'
- Inject into an isolated world (MV3):
- — your code won’t conflict with the page script.file([..])
- Performance:
- Group files in a single call when possible to reduce overhead.run
- Safety:
- Functions passed to should be self-contained: rely only on what’s available in the page context. In MV2 they are string-serialized.
- Timeout error (MV2): increase timeFallback.undefined
- result (MV2): check the page console — the function may have thrown.tabId
- Nothing happens:
- Ensure your extension has permissions for the target tab/frame(s).
- Verify , frameId/documentId, and the runAt timing.world: 'ISOLATED'`.
- Conflicts with page code (MV3): use