-
Notifications
You must be signed in to change notification settings - Fork 65
/
index.js
184 lines (155 loc) · 5.91 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import path from 'node:path'
import { readdir, readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import EventEmitter from 'node:events'
import fs from 'fs-extra'
import semver from 'semver'
import decompress from 'decompress'
import { ExtendedExtensions } from 'electron-extended-webextensions'
// TODO: This smells! Inject options in constructor
import Config from '../config.js'
const { dir: extensionsDir, remote } = Config.extensions
// Handle `app.asar` Electron functionality so that extensions can be referenced on the FS
// Also note that MacOS uses `app-arm64.asar`, so we should target the first `.asar/`
const __dirname = fileURLToPath(new URL('./', import.meta.url)).replace(`.asar${path.sep}`, `.asar.unpacked${path.sep}`)
const DEFAULT_EXTENSION_LOCATION = path.join(__dirname, 'builtins')
const DEFAULT_EXTENSION_LIST_LOCATION = path.join(__dirname, 'builtins.json')
export class Extensions extends EventEmitter {
constructor ({
session,
createWindow,
updateBrowserActions,
builtinsLocation = DEFAULT_EXTENSION_LOCATION,
builtinsListLocation = DEFAULT_EXTENSION_LIST_LOCATION,
storageLocation = extensionsDir
}) {
super()
this.createWindow = createWindow
this.updateBrowserActions = updateBrowserActions
this.session = session
this.builtinsLocation = builtinsLocation
this.builtinsListLocation = builtinsListLocation
this.storageLocation = storageLocation
async function onCreateTab ({ url, popup, openerTabId }) {
const options = { url }
if (popup) options.popup = true
if (openerTabId) options.openerTabId = openerTabId
const window = await createWindow(url, options)
return window.web
}
this.extensions = new ExtendedExtensions(this.session, {
onCreateTab
})
this.extensions.browserActions.on('change', (actions) => updateBrowserActions(null, actions))
this.extensions.browserActions.on('change-tab', (tabId, actions) => updateBrowserActions(tabId, actions))
}
async listActions (window) {
const tabId = window ? window.web.id : null
const actions = await this.extensions.browserActions.list(tabId)
return actions.map((action) => {
const onClick = (clickTabId) => this.extensions.browserActions.click(action.extensionId, clickTabId)
return { ...action, onClick }
})
}
listContextMenuForEvent (webContents, event, params, additionalOpts = {}) {
return this.extensions.contextMenus.getForEvent(webContents, event, params, additionalOpts)
}
get all () {
return [...this.extensions.extensions.values()]
}
async get (id) {
return this.extensions.get(id)
}
async byName (findName) {
return this.all.find(({ name }) => name === findName)
}
async getBackgroundPageByName (name) {
const extension = await this.byName(name)
return this.extensions.getBackgroundPage(extension.id)
}
async loadRemote () {
if (!remote) return
for (const url of remote) {
// TODO: Implement this for different protocols
this.loadFromURL(url)
}
}
async loadExtension (extensionPath) {
const manifestPath = path.join(extensionPath, 'manifest.json')
const manifestData = await fs.readFile(manifestPath, 'utf8')
const { name } = JSON.parse(manifestData)
const exists = await this.byName(name)
if (exists) {
return console.warn('Trying to load extension with existing name from', extensionPath, 'with existing extension:', exists)
}
return this.extensions.loadExtension(extensionPath)
}
async getManifestVersionOnDisk (name) {
const manifestLocation = path.join(this.storageLocation, name, 'manifest.json')
try {
const manifestJSON = await readFile(manifestLocation)
const { version } = JSON.parse(manifestJSON)
return version
} catch (e) {
console.error(`Unable to load manifest for ${name}. ${e.stack}`)
return '0.0.0'
}
}
async extractIfNew (name, info) {
const existingVersion = await this.getManifestVersionOnDisk(name)
const isNew = semver.lt(existingVersion, info.version)
if (!isNew) return false
const zipLocation = path.join(this.builtinsLocation, `${name}.zip`)
const extensionLocation = path.join(this.storageLocation, name)
const decompressOptions = {}
if (info.stripPrefix) {
decompressOptions.map = (file) => {
if (file.path.startsWith(info.stripPrefix)) {
file.path = file.path.slice(info.stripPrefix.length)
}
return file
}
}
await decompress(zipLocation, extensionLocation, decompressOptions)
return true
}
async extractInternal () {
// Read builtins list
const builtinsListJSON = await readFile(this.builtinsListLocation, 'utf8')
console.log({ builtinsListJSON })
const builtins = await JSON.parse(builtinsListJSON)
const builtinsEntries = [...Object.entries(builtins)]
// Extract them all in paralell
await Promise.all(builtinsEntries.map(([name, info]) => {
return this.extractIfNew(name, info)
}))
}
async registerAll (extensionsFolder = this.storageLocation) {
const rawNames = await readdir(extensionsFolder)
const stats = await Promise.all(
rawNames.map(
(name) => fs.stat(
path.join(extensionsFolder, name)
)
)
)
const extensionFolders = rawNames.filter((name, index) => stats[index].isDirectory())
console.log('Loading extensions', extensionFolders)
for (const folder of extensionFolders) {
try {
const extension = await this.loadExtension(path.join(extensionsFolder, folder))
// Must have been skipped
if (!extension) continue
console.log('Loaded extension', extension.manifest)
if (process.env.NODE_ENV === 'debug') {
// TODO: Open devtools?
}
} catch (e) {
console.error('Error loading extension', folder, e)
}
}
}
}
export function createExtensions (opts) {
return new Extensions(opts)
}