firefox-css-theme 0.2.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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/cli.js +101 -0
- package/dist/commands/create.js +34 -0
- package/dist/commands/mcp.js +219 -0
- package/dist/commands/profiles.js +17 -0
- package/dist/commands/save.js +91 -0
- package/dist/commands/start.js +89 -0
- package/dist/commands/styles.js +1 -0
- package/dist/firefox.js +366 -0
- package/dist/index.js +11 -0
- package/dist/processor.js +37 -0
- package/dist/profiles.js +98 -0
- package/dist/registry.js +48 -0
- package/dist/types.js +1 -0
- package/dist/watcher.js +111 -0
- package/package.json +49 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { firefoxManager } from "../firefox.js";
|
|
4
|
+
import { compileCssFile } from "../processor.js";
|
|
5
|
+
import { resolveProfileDirectoryByName } from "../profiles.js";
|
|
6
|
+
import { ROOT_USER_CHROME_ID, ROOT_USER_CONTENT_ID } from "../registry.js";
|
|
7
|
+
import { fileWatcher } from "../watcher.js";
|
|
8
|
+
async function processAndInjectCss(srcPath, destPath, id, target) {
|
|
9
|
+
if (!existsSync(srcPath)) {
|
|
10
|
+
if (target === "content") {
|
|
11
|
+
await firefoxManager.removeContentStyle(id).catch(() => { });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
await firefoxManager.removeChromeStyle(id).catch(() => { });
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const compilationResult = await compileCssFile(srcPath, destPath);
|
|
20
|
+
if (target === "content") {
|
|
21
|
+
await firefoxManager.injectContentStyle(compilationResult.css, id, srcPath);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await firefoxManager.injectChromeStyle(compilationResult.css, id, srcPath);
|
|
25
|
+
}
|
|
26
|
+
console.log(`\x1b[32m[reloaded]\x1b[0m ${id} (${srcPath})`);
|
|
27
|
+
return compilationResult.importedFiles;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`\x1b[31m[build error]\x1b[0m ${srcPath}:`, error instanceof Error ? error.message : error);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Starts a Firefox instance, compiles and injects theme stylesheets, and
|
|
36
|
+
* initiates live watching.
|
|
37
|
+
*/
|
|
38
|
+
export async function startCommand(options = {}) {
|
|
39
|
+
let profileDirectory;
|
|
40
|
+
if (options.profileName) {
|
|
41
|
+
profileDirectory = resolveProfileDirectoryByName(options.profileName);
|
|
42
|
+
console.log(`Using profile: \x1b[1m${options.profileName}\x1b[0m`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log("Using temporary profile.");
|
|
46
|
+
}
|
|
47
|
+
const destChromePath = path.resolve(process.cwd(), ".dist", "userChrome.css");
|
|
48
|
+
const destContentPath = path.resolve(process.cwd(), ".dist", "userContent.css");
|
|
49
|
+
const srcChromePath = path.resolve(process.cwd(), options.chromePath || "userChrome.css");
|
|
50
|
+
const srcContentPath = path.resolve(process.cwd(), options.contentPath || "userContent.css");
|
|
51
|
+
const chromeTarget = {
|
|
52
|
+
filePaths: [srcChromePath],
|
|
53
|
+
onChange: async () => {
|
|
54
|
+
const importedFiles = await processAndInjectCss(srcChromePath, destChromePath, ROOT_USER_CHROME_ID, "chrome");
|
|
55
|
+
fileWatcher.updateFilePaths(chromeTarget, [
|
|
56
|
+
srcChromePath,
|
|
57
|
+
...(importedFiles || []),
|
|
58
|
+
]);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
fileWatcher.addTarget(chromeTarget);
|
|
62
|
+
const contentTarget = {
|
|
63
|
+
filePaths: [srcContentPath],
|
|
64
|
+
onChange: async () => {
|
|
65
|
+
const importedFiles = await processAndInjectCss(srcContentPath, destContentPath, ROOT_USER_CONTENT_ID, "content");
|
|
66
|
+
fileWatcher.updateFilePaths(contentTarget, [
|
|
67
|
+
srcContentPath,
|
|
68
|
+
...(importedFiles || []),
|
|
69
|
+
]);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
fileWatcher.addTarget(contentTarget);
|
|
73
|
+
console.log("Launching Firefox instance...");
|
|
74
|
+
await firefoxManager.initialiseBrowser(options.binaryPath, options.profileName, options.headless, options.novaUi);
|
|
75
|
+
if (options.watch !== false) {
|
|
76
|
+
await fileWatcher.start();
|
|
77
|
+
console.log("Live watcher active. Press Ctrl+C to quit.\n");
|
|
78
|
+
}
|
|
79
|
+
await chromeTarget.onChange();
|
|
80
|
+
await contentTarget.onChange();
|
|
81
|
+
const handleShutdown = async () => {
|
|
82
|
+
console.log("\nShutting down Firefox...");
|
|
83
|
+
await fileWatcher.close().catch(() => { });
|
|
84
|
+
await firefoxManager.terminateBrowser().catch(() => { });
|
|
85
|
+
process.exit(0);
|
|
86
|
+
};
|
|
87
|
+
process.on("SIGINT", handleShutdown);
|
|
88
|
+
process.on("SIGTERM", handleShutdown);
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/firefox.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { Builder, By } from "selenium-webdriver";
|
|
2
|
+
import * as firefox from "selenium-webdriver/firefox.js";
|
|
3
|
+
import { resolveProfileDirectoryByName } from "./profiles.js";
|
|
4
|
+
import { styleRegistry } from "./registry.js";
|
|
5
|
+
export class FirefoxManager {
|
|
6
|
+
driver = null;
|
|
7
|
+
async initialiseBrowser(binaryPath, profileName, headless, novaUi) {
|
|
8
|
+
if (this.driver)
|
|
9
|
+
return;
|
|
10
|
+
const isNovaUiEnabled = novaUi ?? process.argv.includes("--nova-ui");
|
|
11
|
+
const isHeadlessEnabled = headless ?? process.argv.includes("--headless");
|
|
12
|
+
const options = new firefox.Options();
|
|
13
|
+
options.addArguments("-no-remote", "-new-instance");
|
|
14
|
+
if (isHeadlessEnabled)
|
|
15
|
+
options.addArguments("-headless");
|
|
16
|
+
options.setPreference("devtools.chrome.enabled", true);
|
|
17
|
+
options.setPreference("devtools.debugger.remote-enabled", true);
|
|
18
|
+
options.setPreference("devtools.debugger.prompt-connection", false);
|
|
19
|
+
options.setPreference("toolkit.legacyUserProfileCustomizations.stylesheets", true);
|
|
20
|
+
options.setPreference("marionette.allow-system-access", true);
|
|
21
|
+
options.setPreference("remote.allow-system-access", true);
|
|
22
|
+
options.setPreference("browser.nova.enabled", isNovaUiEnabled);
|
|
23
|
+
options.setPreference("browser.newtabpage.activity-stream.nova.enabled", isNovaUiEnabled);
|
|
24
|
+
if (binaryPath)
|
|
25
|
+
options.setBinary(binaryPath);
|
|
26
|
+
if (profileName) {
|
|
27
|
+
const profileDirectory = resolveProfileDirectoryByName(profileName);
|
|
28
|
+
options.setProfile(profileDirectory);
|
|
29
|
+
}
|
|
30
|
+
const service = new firefox.ServiceBuilder()
|
|
31
|
+
.enableVerboseLogging(true)
|
|
32
|
+
.addArguments("--allow-system-access");
|
|
33
|
+
try {
|
|
34
|
+
this.driver = await new Builder()
|
|
35
|
+
.forBrowser("firefox")
|
|
36
|
+
.setFirefoxOptions(options)
|
|
37
|
+
.setFirefoxService(service)
|
|
38
|
+
.build();
|
|
39
|
+
await this.ensureChromeContext();
|
|
40
|
+
if (!isHeadlessEnabled) {
|
|
41
|
+
await this.openBrowserToolbox().catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (this.driver) {
|
|
46
|
+
await this.driver.quit().catch(() => { });
|
|
47
|
+
this.driver = null;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async terminateBrowser() {
|
|
53
|
+
if (!this.driver)
|
|
54
|
+
return;
|
|
55
|
+
await this.driver.quit();
|
|
56
|
+
this.driver = null;
|
|
57
|
+
}
|
|
58
|
+
async ensureChromeContext() {
|
|
59
|
+
if (!this.driver) {
|
|
60
|
+
throw new Error("Firefox driver instance is not initialised.");
|
|
61
|
+
}
|
|
62
|
+
await this.driver.setContext("chrome");
|
|
63
|
+
}
|
|
64
|
+
async executeChromeScript(script, ...argumentsList) {
|
|
65
|
+
await this.ensureChromeContext();
|
|
66
|
+
return (await this.driver.executeScript(script, ...argumentsList));
|
|
67
|
+
}
|
|
68
|
+
async queryElements(selector) {
|
|
69
|
+
await this.ensureChromeContext();
|
|
70
|
+
return await this.executeChromeScript((querySelector) => {
|
|
71
|
+
const matchedElements = Array.from(document.querySelectorAll(querySelector));
|
|
72
|
+
return matchedElements.map((element) => {
|
|
73
|
+
const attributeMap = {};
|
|
74
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
75
|
+
attributeMap[attribute.name] = attribute.value;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
tagName: element.tagName.toLowerCase(),
|
|
79
|
+
id: element.id || "",
|
|
80
|
+
className: element.className || "",
|
|
81
|
+
childCount: element.children.length,
|
|
82
|
+
textContent: (element.textContent || "")
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 100),
|
|
85
|
+
attributes: attributeMap,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}, selector);
|
|
89
|
+
}
|
|
90
|
+
async getComputedStyles(selector, stylePropertyNames) {
|
|
91
|
+
await this.ensureChromeContext();
|
|
92
|
+
return await this.executeChromeScript((targetSelector, requestedProperties) => {
|
|
93
|
+
const targetElement = document.querySelector(targetSelector);
|
|
94
|
+
if (!targetElement) {
|
|
95
|
+
throw new Error("Element not found for selector: " + targetSelector);
|
|
96
|
+
}
|
|
97
|
+
const computedStyleDeclaration = window.getComputedStyle(targetElement);
|
|
98
|
+
const styleResult = {};
|
|
99
|
+
if (Array.isArray(requestedProperties) &&
|
|
100
|
+
requestedProperties.length > 0) {
|
|
101
|
+
for (const propertyName of requestedProperties) {
|
|
102
|
+
styleResult[propertyName] =
|
|
103
|
+
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (let index = 0; index < computedStyleDeclaration.length; index++) {
|
|
108
|
+
const propertyName = computedStyleDeclaration[index];
|
|
109
|
+
styleResult[propertyName] =
|
|
110
|
+
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return styleResult;
|
|
114
|
+
}, selector, stylePropertyNames);
|
|
115
|
+
}
|
|
116
|
+
async getUiTree(rootSelector = "window", maxDepth = 3) {
|
|
117
|
+
await this.ensureChromeContext();
|
|
118
|
+
return await this.executeChromeScript((targetSelector, maximumTraversalDepth) => {
|
|
119
|
+
const rootTarget = targetSelector === "window"
|
|
120
|
+
? document.documentElement
|
|
121
|
+
: document.querySelector(targetSelector);
|
|
122
|
+
if (!rootTarget) {
|
|
123
|
+
throw new Error("Root element not found for selector: " +
|
|
124
|
+
targetSelector);
|
|
125
|
+
}
|
|
126
|
+
const rootNodeHierarchy = {
|
|
127
|
+
tagName: rootTarget.tagName.toLowerCase(),
|
|
128
|
+
id: rootTarget.id || "",
|
|
129
|
+
className: rootTarget.className || "",
|
|
130
|
+
attributes: {},
|
|
131
|
+
children: [],
|
|
132
|
+
};
|
|
133
|
+
for (const attribute of Array.from(rootTarget.attributes)) {
|
|
134
|
+
rootNodeHierarchy.attributes[attribute.name] =
|
|
135
|
+
attribute.value;
|
|
136
|
+
}
|
|
137
|
+
const nodeQueue = [
|
|
138
|
+
{
|
|
139
|
+
domNode: rootTarget,
|
|
140
|
+
hierarchyNode: rootNodeHierarchy,
|
|
141
|
+
currentDepth: 0,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
while (nodeQueue.length > 0) {
|
|
145
|
+
const queueItem = nodeQueue.shift();
|
|
146
|
+
if (!queueItem ||
|
|
147
|
+
queueItem.currentDepth >= maximumTraversalDepth) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const childElement of Array.from(queueItem.domNode.children)) {
|
|
151
|
+
const childAttributes = {};
|
|
152
|
+
for (const attribute of Array.from(childElement.attributes)) {
|
|
153
|
+
childAttributes[attribute.name] = attribute.value;
|
|
154
|
+
}
|
|
155
|
+
const childHierarchyNode = {
|
|
156
|
+
tagName: childElement.tagName.toLowerCase(),
|
|
157
|
+
id: childElement.id || "",
|
|
158
|
+
className: childElement.className || "",
|
|
159
|
+
attributes: childAttributes,
|
|
160
|
+
children: [],
|
|
161
|
+
};
|
|
162
|
+
queueItem.hierarchyNode.children.push(childHierarchyNode);
|
|
163
|
+
nodeQueue.push({
|
|
164
|
+
domNode: childElement,
|
|
165
|
+
hierarchyNode: childHierarchyNode,
|
|
166
|
+
currentDepth: queueItem.currentDepth + 1,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return rootNodeHierarchy;
|
|
171
|
+
}, rootSelector, maxDepth);
|
|
172
|
+
}
|
|
173
|
+
async customiseToolbar(action = "enter") {
|
|
174
|
+
await this.ensureChromeContext();
|
|
175
|
+
return await this.executeChromeScript(async (requestedAction) => {
|
|
176
|
+
const customizeMode = window.gCustomizeMode;
|
|
177
|
+
const documentElement = document.documentElement;
|
|
178
|
+
if (!customizeMode) {
|
|
179
|
+
const customizeCommand = document.getElementById("cmd_CustomizeToolbars");
|
|
180
|
+
if (customizeCommand) {
|
|
181
|
+
customizeCommand.doCommand();
|
|
182
|
+
return {
|
|
183
|
+
isCustomising: documentElement.hasAttribute("customizing"),
|
|
184
|
+
success: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
throw new Error("Customise toolbar mode is not available in the current context.");
|
|
188
|
+
}
|
|
189
|
+
const isCurrentlyCustomising = documentElement.hasAttribute("customizing") ||
|
|
190
|
+
Boolean(customizeMode.visible);
|
|
191
|
+
const shouldEnter = requestedAction === "enter";
|
|
192
|
+
if (shouldEnter !== isCurrentlyCustomising) {
|
|
193
|
+
const transitionEventName = shouldEnter
|
|
194
|
+
? "customizationready"
|
|
195
|
+
: "aftercustomization";
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
const timeout = setTimeout(resolve, 5000);
|
|
198
|
+
window.addEventListener(transitionEventName, () => {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
resolve();
|
|
201
|
+
}, { once: true });
|
|
202
|
+
customizeMode[requestedAction]();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
isCustomising: documentElement.hasAttribute("customizing") ||
|
|
207
|
+
Boolean(customizeMode.visible),
|
|
208
|
+
success: true,
|
|
209
|
+
};
|
|
210
|
+
}, action);
|
|
211
|
+
}
|
|
212
|
+
async customizeToolbar(action = "enter") {
|
|
213
|
+
return this.customiseToolbar(action);
|
|
214
|
+
}
|
|
215
|
+
async injectChromeStyle(css, id = "mcp-injected-style", srcPath) {
|
|
216
|
+
await this.ensureChromeContext();
|
|
217
|
+
const result = await this.executeChromeScript((id, cssSource) => {
|
|
218
|
+
let existingStyleElement = document.getElementById(id);
|
|
219
|
+
if (!existingStyleElement) {
|
|
220
|
+
existingStyleElement = document.createElement("style");
|
|
221
|
+
existingStyleElement.id = id;
|
|
222
|
+
existingStyleElement.setAttribute("type", "text/css");
|
|
223
|
+
document.documentElement.appendChild(existingStyleElement);
|
|
224
|
+
}
|
|
225
|
+
existingStyleElement.textContent = cssSource;
|
|
226
|
+
return { id: id, success: true };
|
|
227
|
+
}, id, css);
|
|
228
|
+
styleRegistry.register(id, srcPath, "chrome");
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
async injectContentStyle(css, id = "mcp-injected-content-style", srcPath) {
|
|
232
|
+
await this.ensureChromeContext();
|
|
233
|
+
const result = await this.executeChromeScript((id, cssSource) => {
|
|
234
|
+
const win = window;
|
|
235
|
+
win.__injectedContentSheets =
|
|
236
|
+
win.__injectedContentSheets || new Map();
|
|
237
|
+
const prevUriStr = win.__injectedContentSheets.get(id);
|
|
238
|
+
const sss = globalThis.Cc["@mozilla.org/content/style-sheet-service;1"].getService(globalThis.Ci.nsIStyleSheetService);
|
|
239
|
+
const ioService = globalThis.Cc["@mozilla.org/network/io-service;1"].getService(globalThis.Ci.nsIIOService);
|
|
240
|
+
if (prevUriStr) {
|
|
241
|
+
try {
|
|
242
|
+
const prevUri = ioService.newURI(prevUriStr);
|
|
243
|
+
if (sss.sheetRegistered(prevUri, sss.USER_SHEET)) {
|
|
244
|
+
sss.unregisterSheet(prevUri, sss.USER_SHEET);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch { }
|
|
248
|
+
}
|
|
249
|
+
const newUriStr = "data:text/css;charset=utf-8," +
|
|
250
|
+
encodeURIComponent(cssSource);
|
|
251
|
+
const newUri = ioService.newURI(newUriStr);
|
|
252
|
+
if (!sss.sheetRegistered(newUri, sss.USER_SHEET)) {
|
|
253
|
+
sss.loadAndRegisterSheet(newUri, sss.USER_SHEET);
|
|
254
|
+
}
|
|
255
|
+
win.__injectedContentSheets.set(id, newUriStr);
|
|
256
|
+
return { id: id, success: true };
|
|
257
|
+
}, id, css);
|
|
258
|
+
styleRegistry.register(id, srcPath, "content");
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
async removeChromeStyle(id) {
|
|
262
|
+
await this.ensureChromeContext();
|
|
263
|
+
const result = await this.executeChromeScript((id) => {
|
|
264
|
+
const targetStyleElement = document.getElementById(id);
|
|
265
|
+
if (targetStyleElement) {
|
|
266
|
+
targetStyleElement.remove();
|
|
267
|
+
return { id: id, removed: true };
|
|
268
|
+
}
|
|
269
|
+
return { id: id, removed: false };
|
|
270
|
+
}, id);
|
|
271
|
+
styleRegistry.unregister(id);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
async removeContentStyle(id) {
|
|
275
|
+
await this.ensureChromeContext();
|
|
276
|
+
const result = await this.executeChromeScript((id) => {
|
|
277
|
+
let removed = false;
|
|
278
|
+
const win = window;
|
|
279
|
+
if (win.__injectedContentSheets?.has(id)) {
|
|
280
|
+
const uriStr = win.__injectedContentSheets.get(id);
|
|
281
|
+
try {
|
|
282
|
+
const sss = globalThis.Cc["@mozilla.org/content/style-sheet-service;1"].getService(globalThis.Ci.nsIStyleSheetService);
|
|
283
|
+
const ioService = globalThis.Cc["@mozilla.org/network/io-service;1"].getService(globalThis.Ci.nsIIOService);
|
|
284
|
+
const uri = ioService.newURI(uriStr);
|
|
285
|
+
if (sss.sheetRegistered(uri, sss.USER_SHEET)) {
|
|
286
|
+
sss.unregisterSheet(uri, sss.USER_SHEET);
|
|
287
|
+
}
|
|
288
|
+
removed = true;
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
win.__injectedContentSheets.delete(id);
|
|
292
|
+
}
|
|
293
|
+
return { id: id, removed: removed };
|
|
294
|
+
}, id);
|
|
295
|
+
styleRegistry.unregister(id);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
async listChromeStyles() {
|
|
299
|
+
await this.ensureChromeContext();
|
|
300
|
+
const domStyles = await this.executeChromeScript(() => {
|
|
301
|
+
const styleElements = Array.from(document.querySelectorAll("style[id]"));
|
|
302
|
+
return styleElements.map((element) => ({
|
|
303
|
+
id: element.id,
|
|
304
|
+
length: element.textContent ? element.textContent.length : 0,
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
return domStyles.map((item) => {
|
|
308
|
+
const registration = styleRegistry.get(item.id);
|
|
309
|
+
return {
|
|
310
|
+
id: item.id,
|
|
311
|
+
length: item.length,
|
|
312
|
+
srcPath: registration?.srcPath,
|
|
313
|
+
target: "chrome",
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async listContentStyles() {
|
|
318
|
+
await this.ensureChromeContext();
|
|
319
|
+
const contentStyles = await this.executeChromeScript(() => {
|
|
320
|
+
const win = window;
|
|
321
|
+
const styles = [];
|
|
322
|
+
if (win.__injectedContentSheets) {
|
|
323
|
+
for (const [id, uriStr,] of win.__injectedContentSheets.entries()) {
|
|
324
|
+
styles.push({
|
|
325
|
+
id: id,
|
|
326
|
+
length: decodeURIComponent(uriStr.replace(/^data:text\/css;charset=utf-8,/, "")).length,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return styles;
|
|
331
|
+
});
|
|
332
|
+
return contentStyles.map((item) => {
|
|
333
|
+
const registration = styleRegistry.get(item.id);
|
|
334
|
+
return {
|
|
335
|
+
id: item.id,
|
|
336
|
+
length: item.length,
|
|
337
|
+
srcPath: registration?.srcPath,
|
|
338
|
+
target: "content",
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async getScreenshot(targetSelector) {
|
|
343
|
+
await this.ensureChromeContext();
|
|
344
|
+
if (targetSelector) {
|
|
345
|
+
const targetWebElement = await this.driver.findElement(By.css(targetSelector));
|
|
346
|
+
const elementScreenshotBase64 = await targetWebElement.takeScreenshot();
|
|
347
|
+
return { base64Image: elementScreenshotBase64, format: "png" };
|
|
348
|
+
}
|
|
349
|
+
const fullWindowScreenshotBase64 = await this.driver.takeScreenshot();
|
|
350
|
+
return { base64Image: fullWindowScreenshotBase64, format: "png" };
|
|
351
|
+
}
|
|
352
|
+
async openBrowserToolbox() {
|
|
353
|
+
await this.ensureChromeContext();
|
|
354
|
+
return await this.executeChromeScript(() => {
|
|
355
|
+
try {
|
|
356
|
+
const { BrowserToolboxLauncher } = ChromeUtils.importESModule("resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs");
|
|
357
|
+
BrowserToolboxLauncher.init();
|
|
358
|
+
return { success: true };
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return { success: false };
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export const firefoxManager = new FirefoxManager();
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { FirefoxManager, firefoxManager } from "./firefox.js";
|
|
3
|
+
export { compileCssFile, compileCssString } from "./processor.js";
|
|
4
|
+
export { ROOT_USER_CHROME_ID, ROOT_USER_CONTENT_ID, StyleRegistry, styleRegistry, } from "./registry.js";
|
|
5
|
+
export { getFirefoxConfigurationDirectory, listFirefoxProfiles, resolveProfileDirectoryByName, } from "./profiles.js";
|
|
6
|
+
export { FileWatcher, fileWatcher } from "./watcher.js";
|
|
7
|
+
export { createCommand } from "./commands/create.js";
|
|
8
|
+
export { startCommand } from "./commands/start.js";
|
|
9
|
+
export { saveCommand } from "./commands/save.js";
|
|
10
|
+
export { profilesCommand } from "./commands/profiles.js";
|
|
11
|
+
export { startMcpServer } from "./commands/mcp.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import postcssImport from "postcss-import";
|
|
5
|
+
/** Compiles a CSS string using PostCSS and inlines @import dependencies. */
|
|
6
|
+
export async function compileCssString(css, basePath = path.resolve(process.cwd(), "style.css")) {
|
|
7
|
+
const processor = postcss([postcssImport()]);
|
|
8
|
+
const output = await processor.process(css, { from: basePath });
|
|
9
|
+
const importedFiles = [];
|
|
10
|
+
for (const message of output.messages) {
|
|
11
|
+
if (message.type === "dependency" && typeof message.file === "string") {
|
|
12
|
+
importedFiles.push(message.file);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { css: output.css, importedFiles: importedFiles };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compiles a CSS file, inlines @import rules via PostCSS, and optionally writes
|
|
19
|
+
* the output bundle to a destination path.
|
|
20
|
+
*/
|
|
21
|
+
export async function compileCssFile(srcPath, destPath) {
|
|
22
|
+
const absoluteSrcPath = path.resolve(process.cwd(), srcPath);
|
|
23
|
+
if (!existsSync(absoluteSrcPath)) {
|
|
24
|
+
throw new Error(`Stylesheet file does not exist at path: ${absoluteSrcPath}`);
|
|
25
|
+
}
|
|
26
|
+
const sourceContent = readFileSync(absoluteSrcPath, "utf8");
|
|
27
|
+
const result = await compileCssString(sourceContent, absoluteSrcPath);
|
|
28
|
+
if (destPath) {
|
|
29
|
+
const absoluteDestPath = path.resolve(process.cwd(), destPath);
|
|
30
|
+
const destDir = path.dirname(absoluteDestPath);
|
|
31
|
+
if (!existsSync(destDir)) {
|
|
32
|
+
mkdirSync(destDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(absoluteDestPath, result.css, "utf8");
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Discovers the platform-specific directory where Firefox configuration and
|
|
6
|
+
* profiles are located.
|
|
7
|
+
*/
|
|
8
|
+
export function getFirefoxConfigurationDirectory() {
|
|
9
|
+
const platformName = process.platform;
|
|
10
|
+
if (platformName === "darwin") {
|
|
11
|
+
return path.join(os.homedir(), "Library", "Application Support", "Firefox");
|
|
12
|
+
}
|
|
13
|
+
if (platformName === "win32") {
|
|
14
|
+
const applicationDataDirectory = process.env.APPDATA ||
|
|
15
|
+
path.join(os.homedir(), "AppData", "Roaming");
|
|
16
|
+
return path.join(applicationDataDirectory, "Mozilla", "Firefox");
|
|
17
|
+
}
|
|
18
|
+
return path.join(os.homedir(), ".mozilla", "firefox");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses the Firefox profiles.ini configuration file to extract profile
|
|
22
|
+
* records.
|
|
23
|
+
*/
|
|
24
|
+
export function listFirefoxProfiles(customConfigurationDirectory) {
|
|
25
|
+
const configurationDirectory = customConfigurationDirectory || getFirefoxConfigurationDirectory();
|
|
26
|
+
const profilesIniPath = path.join(configurationDirectory, "profiles.ini");
|
|
27
|
+
if (!existsSync(profilesIniPath))
|
|
28
|
+
return [];
|
|
29
|
+
const fileContent = readFileSync(profilesIniPath, "utf8");
|
|
30
|
+
const iniLines = fileContent.split(/\r?\n/);
|
|
31
|
+
const profileRecords = [];
|
|
32
|
+
let currentSection = null;
|
|
33
|
+
let currentProfileData = {};
|
|
34
|
+
for (const line of iniLines) {
|
|
35
|
+
const trimmedLine = line.trim();
|
|
36
|
+
if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
|
|
37
|
+
if (currentSection &&
|
|
38
|
+
currentSection.toLowerCase().startsWith("profile") &&
|
|
39
|
+
currentProfileData.name &&
|
|
40
|
+
currentProfileData.path) {
|
|
41
|
+
const isRelative = currentProfileData.isRelative ?? true;
|
|
42
|
+
const resolvedPath = isRelative
|
|
43
|
+
? path.resolve(configurationDirectory, currentProfileData.path)
|
|
44
|
+
: currentProfileData.path;
|
|
45
|
+
profileRecords.push({
|
|
46
|
+
name: currentProfileData.name,
|
|
47
|
+
path: resolvedPath,
|
|
48
|
+
isRelative: isRelative,
|
|
49
|
+
isDefault: Boolean(currentProfileData.isDefault),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
currentSection = trimmedLine.slice(1, -1);
|
|
53
|
+
currentProfileData = {};
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const equalsIndex = trimmedLine.indexOf("=");
|
|
57
|
+
if (equalsIndex === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = trimmedLine.slice(0, equalsIndex).trim();
|
|
60
|
+
const value = trimmedLine.slice(equalsIndex + 1).trim();
|
|
61
|
+
if (key === "Name")
|
|
62
|
+
currentProfileData.name = value;
|
|
63
|
+
if (key === "Path")
|
|
64
|
+
currentProfileData.path = value;
|
|
65
|
+
if (key === "IsRelative")
|
|
66
|
+
currentProfileData.isRelative = value === "1";
|
|
67
|
+
if (key === "Default")
|
|
68
|
+
currentProfileData.isDefault = value === "1";
|
|
69
|
+
}
|
|
70
|
+
if (currentSection &&
|
|
71
|
+
currentSection.toLowerCase().startsWith("profile") &&
|
|
72
|
+
currentProfileData.name &&
|
|
73
|
+
currentProfileData.path) {
|
|
74
|
+
const isRelative = currentProfileData.isRelative ?? true;
|
|
75
|
+
const resolvedPath = isRelative
|
|
76
|
+
? path.resolve(configurationDirectory, currentProfileData.path)
|
|
77
|
+
: currentProfileData.path;
|
|
78
|
+
profileRecords.push({
|
|
79
|
+
name: currentProfileData.name,
|
|
80
|
+
path: resolvedPath,
|
|
81
|
+
isRelative: isRelative,
|
|
82
|
+
isDefault: Boolean(currentProfileData.isDefault),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return profileRecords;
|
|
86
|
+
}
|
|
87
|
+
/** Resolves a registered Firefox profile name to its filesystem directory path. */
|
|
88
|
+
export function resolveProfileDirectoryByName(profileName) {
|
|
89
|
+
const profiles = listFirefoxProfiles();
|
|
90
|
+
const matchedProfile = profiles.find((profile) => profile.name === profileName);
|
|
91
|
+
if (!matchedProfile) {
|
|
92
|
+
const availableProfileNames = profiles
|
|
93
|
+
.map((profile) => profile.name)
|
|
94
|
+
.join(", ");
|
|
95
|
+
throw new Error(`Profile "${profileName}" not found. Available profiles: ${availableProfileNames || "none"}`);
|
|
96
|
+
}
|
|
97
|
+
return matchedProfile.path;
|
|
98
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const ROOT_USER_CHROME_ID = "root:userChrome";
|
|
3
|
+
export const ROOT_USER_CONTENT_ID = "root:userContent";
|
|
4
|
+
export class StyleRegistry {
|
|
5
|
+
styles = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Generates a deterministic short hash ID for a file path when no explicit
|
|
8
|
+
* ID is supplied.
|
|
9
|
+
*/
|
|
10
|
+
generateIdForFile(srcPath) {
|
|
11
|
+
const hash = createHash("sha256")
|
|
12
|
+
.update(srcPath, "utf8")
|
|
13
|
+
.digest("hex")
|
|
14
|
+
.slice(0, 8);
|
|
15
|
+
return `id-${hash}`;
|
|
16
|
+
}
|
|
17
|
+
/** Registers or updates a style entry in the in-memory registry. */
|
|
18
|
+
register(id, srcPath, target = "chrome") {
|
|
19
|
+
this.styles.set(id, {
|
|
20
|
+
registeredAt: new Date(),
|
|
21
|
+
srcPath: srcPath,
|
|
22
|
+
target: target,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Unregisters a style entry from the in-memory registry, returning whether
|
|
27
|
+
* it was found.
|
|
28
|
+
*/
|
|
29
|
+
unregister(id) {
|
|
30
|
+
return this.styles.delete(id);
|
|
31
|
+
}
|
|
32
|
+
/** Retrieves metadata for a registered style. */
|
|
33
|
+
get(id) {
|
|
34
|
+
return this.styles.get(id);
|
|
35
|
+
}
|
|
36
|
+
/** Returns registered styles with optional target filtering. */
|
|
37
|
+
list(targetFilter) {
|
|
38
|
+
return Array.from(this.styles.entries())
|
|
39
|
+
.filter(([, entry]) => !targetFilter || entry.target === targetFilter)
|
|
40
|
+
.map(([id, entry]) => ({
|
|
41
|
+
id: id,
|
|
42
|
+
registeredAt: entry.registeredAt,
|
|
43
|
+
srcPath: entry.srcPath,
|
|
44
|
+
target: entry.target,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export const styleRegistry = new StyleRegistry();
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|