@multiplatform.one/vite-plugin-webext 6.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 +201 -0
- package/lib/index.cjs +336 -0
- package/lib/index.mjs +296 -0
- package/package.json +68 -0
- package/src/index.ts +412 -0
- package/types/index.d.ts +75 -0
- package/types/index.d.ts.map +1 -0
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lookupProjectRoot, lookupTamaguiModules, oneServerOnlyBrowserStub, stubExpoTypeDeclarations, workspaceSourceAliases } from "@multiplatform.one/utils/dev";
|
|
4
|
+
import react from "@vitejs/plugin-react";
|
|
5
|
+
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
const WEBEXT_EXTENSIONS = [
|
|
8
|
+
".webext.ts",
|
|
9
|
+
".webext.tsx",
|
|
10
|
+
".webext.js",
|
|
11
|
+
".webext.jsx",
|
|
12
|
+
".web.ts",
|
|
13
|
+
".web.tsx",
|
|
14
|
+
".web.js",
|
|
15
|
+
".web.jsx",
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".js",
|
|
19
|
+
".jsx"
|
|
20
|
+
];
|
|
21
|
+
const OPTIMIZE_INCLUDE = [
|
|
22
|
+
"react",
|
|
23
|
+
"react-dom",
|
|
24
|
+
"webextension-polyfill",
|
|
25
|
+
"@tamagui/core",
|
|
26
|
+
"@tamagui/web",
|
|
27
|
+
"tamagui",
|
|
28
|
+
"react-native-web",
|
|
29
|
+
"@multiplatform.one/components"
|
|
30
|
+
];
|
|
31
|
+
/** shiki's chunk graph (wasm/TLA) can't fold into the single-file iife a
|
|
32
|
+
* content script requires. Stub it with an empty module —
|
|
33
|
+
* @multiplatform.one/components' highlightCode() catches the missing
|
|
34
|
+
* codeToTokens and falls back to plain text. */
|
|
35
|
+
function shikiStubPlugin() {
|
|
36
|
+
const VIRTUAL_ID = "\0webext-shiki-stub";
|
|
37
|
+
return {
|
|
38
|
+
name: "webext-shiki-stub",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
resolveId(id) {
|
|
41
|
+
if (/^shiki(\/.*)?$/.test(id)) return VIRTUAL_ID;
|
|
42
|
+
},
|
|
43
|
+
load(id) {
|
|
44
|
+
if (id === VIRTUAL_ID) return "export {}";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveDefaults(options) {
|
|
49
|
+
return {
|
|
50
|
+
workspaceRoot: options.workspaceRoot ?? lookupProjectRoot(),
|
|
51
|
+
isDev: options.isDev ?? process.env.NODE_ENV !== "production",
|
|
52
|
+
port: options.port ?? (Number(process.env.PORT) || 3303)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function baseDefine(options, isDev) {
|
|
56
|
+
return {
|
|
57
|
+
__DEV__: isDev,
|
|
58
|
+
__NAME__: JSON.stringify(options.packageInfo.name),
|
|
59
|
+
"process.env.NODE_ENV": JSON.stringify(isDev ? "development" : "production"),
|
|
60
|
+
global: "globalThis",
|
|
61
|
+
...options.define
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function baseResolve(options, workspaceRoot) {
|
|
65
|
+
return {
|
|
66
|
+
alias: [
|
|
67
|
+
...Object.entries(workspaceSourceAliases(workspaceRoot)).map(([find, replacement]) => ({
|
|
68
|
+
find,
|
|
69
|
+
replacement
|
|
70
|
+
})),
|
|
71
|
+
{
|
|
72
|
+
find: "react-native",
|
|
73
|
+
replacement: "react-native-web"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
find: "@",
|
|
77
|
+
replacement: options.targetDir
|
|
78
|
+
},
|
|
79
|
+
...options.aliases ?? []
|
|
80
|
+
],
|
|
81
|
+
extensions: WEBEXT_EXTENSIONS,
|
|
82
|
+
mainFields: [
|
|
83
|
+
"browser",
|
|
84
|
+
"module",
|
|
85
|
+
"main"
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function baseOptimizeDeps() {
|
|
90
|
+
return {
|
|
91
|
+
include: OPTIMIZE_INCLUDE,
|
|
92
|
+
esbuildOptions: {
|
|
93
|
+
resolveExtensions: WEBEXT_EXTENSIONS,
|
|
94
|
+
mainFields: ["module", "main"]
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Discover view names — every directory under <targetDir>/views with an
|
|
99
|
+
* index.html becomes an extension page (popup, options, sidepanel, …). */
|
|
100
|
+
function listWebextViews(targetDir) {
|
|
101
|
+
const viewsDir = path.join(targetDir, "views");
|
|
102
|
+
if (!fs.existsSync(viewsDir)) return [];
|
|
103
|
+
return fs.readdirSync(viewsDir).filter((file) => fs.statSync(path.join(viewsDir, file)).isDirectory()).filter((view) => fs.existsSync(path.join(viewsDir, view, "index.html")));
|
|
104
|
+
}
|
|
105
|
+
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
106
|
+
async function createWebextViewsConfig(options) {
|
|
107
|
+
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
108
|
+
const { tamaguiPlugin } = await import("@tamagui/vite-plugin");
|
|
109
|
+
const views = listWebextViews(options.targetDir);
|
|
110
|
+
return {
|
|
111
|
+
root: options.targetDir,
|
|
112
|
+
base: isDev ? `http://localhost:${port}/` : "/dist/",
|
|
113
|
+
plugins: [
|
|
114
|
+
stubExpoTypeDeclarations(),
|
|
115
|
+
oneServerOnlyBrowserStub(workspaceRoot),
|
|
116
|
+
react({ jsxRuntime: "automatic" }),
|
|
117
|
+
tamaguiPlugin({
|
|
118
|
+
components: lookupTamaguiModules([options.targetDir]),
|
|
119
|
+
config: options.tamaguiConfig,
|
|
120
|
+
outputCSS: path.join(options.targetDir, "tamagui.css")
|
|
121
|
+
})
|
|
122
|
+
],
|
|
123
|
+
define: baseDefine(options, isDev),
|
|
124
|
+
optimizeDeps: baseOptimizeDeps(),
|
|
125
|
+
esbuild: {
|
|
126
|
+
jsx: "automatic",
|
|
127
|
+
target: "esnext"
|
|
128
|
+
},
|
|
129
|
+
resolve: baseResolve(options, workspaceRoot),
|
|
130
|
+
server: {
|
|
131
|
+
port,
|
|
132
|
+
hmr: {
|
|
133
|
+
host: "localhost",
|
|
134
|
+
protocol: "ws",
|
|
135
|
+
clientPort: port
|
|
136
|
+
},
|
|
137
|
+
origin: `http://localhost:${port}`,
|
|
138
|
+
cors: true,
|
|
139
|
+
headers: { "Access-Control-Allow-Origin": "*" }
|
|
140
|
+
},
|
|
141
|
+
build: {
|
|
142
|
+
watch: isDev ? {} : void 0,
|
|
143
|
+
outDir: path.join(options.stagingDir, "dist"),
|
|
144
|
+
emptyOutDir: false,
|
|
145
|
+
sourcemap: isDev ? "inline" : false,
|
|
146
|
+
terserOptions: { mangle: false },
|
|
147
|
+
rollupOptions: { input: Object.fromEntries(views.map((view) => [view, path.join(options.targetDir, "views", view, "index.html")])) }
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** The background service-worker (chromium) / background-script (firefox)
|
|
152
|
+
* build — a single-file iife, no DOM. */
|
|
153
|
+
function createWebextBackgroundConfig(options) {
|
|
154
|
+
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
155
|
+
return {
|
|
156
|
+
root: options.targetDir,
|
|
157
|
+
plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
|
|
158
|
+
define: baseDefine(options, isDev),
|
|
159
|
+
optimizeDeps: baseOptimizeDeps(),
|
|
160
|
+
esbuild: {
|
|
161
|
+
jsx: "automatic",
|
|
162
|
+
target: "esnext"
|
|
163
|
+
},
|
|
164
|
+
resolve: baseResolve(options, workspaceRoot),
|
|
165
|
+
build: {
|
|
166
|
+
watch: isDev ? {} : void 0,
|
|
167
|
+
outDir: path.join(options.stagingDir, "dist/background"),
|
|
168
|
+
cssCodeSplit: false,
|
|
169
|
+
emptyOutDir: false,
|
|
170
|
+
sourcemap: isDev ? "inline" : false,
|
|
171
|
+
lib: {
|
|
172
|
+
entry: path.join(options.targetDir, "background/main.ts"),
|
|
173
|
+
name: options.packageInfo.name,
|
|
174
|
+
formats: ["iife"]
|
|
175
|
+
},
|
|
176
|
+
rollupOptions: { output: {
|
|
177
|
+
entryFileNames: "index.mjs",
|
|
178
|
+
extend: true
|
|
179
|
+
} }
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The content-script build — a single-file iife injected into pages, with
|
|
184
|
+
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
185
|
+
async function createWebextContentConfig(options) {
|
|
186
|
+
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
187
|
+
const { tamaguiPlugin } = await import("@tamagui/vite-plugin");
|
|
188
|
+
return {
|
|
189
|
+
root: options.targetDir,
|
|
190
|
+
plugins: [
|
|
191
|
+
stubExpoTypeDeclarations(),
|
|
192
|
+
oneServerOnlyBrowserStub(workspaceRoot),
|
|
193
|
+
shikiStubPlugin(),
|
|
194
|
+
tamaguiPlugin({
|
|
195
|
+
components: lookupTamaguiModules([options.targetDir]),
|
|
196
|
+
config: options.tamaguiConfig,
|
|
197
|
+
outputCSS: path.join(options.targetDir, "tamagui.content.css")
|
|
198
|
+
})
|
|
199
|
+
],
|
|
200
|
+
define: {
|
|
201
|
+
...baseDefine(options, isDev),
|
|
202
|
+
process: { env: { NODE_ENV: JSON.stringify(isDev ? "development" : "production") } }
|
|
203
|
+
},
|
|
204
|
+
optimizeDeps: baseOptimizeDeps(),
|
|
205
|
+
esbuild: {
|
|
206
|
+
jsx: "automatic",
|
|
207
|
+
target: "esnext"
|
|
208
|
+
},
|
|
209
|
+
resolve: baseResolve(options, workspaceRoot),
|
|
210
|
+
build: {
|
|
211
|
+
watch: isDev ? {} : void 0,
|
|
212
|
+
outDir: path.join(options.stagingDir, "dist/contentScripts"),
|
|
213
|
+
cssCodeSplit: false,
|
|
214
|
+
emptyOutDir: false,
|
|
215
|
+
sourcemap: isDev ? "inline" : false,
|
|
216
|
+
lib: {
|
|
217
|
+
entry: path.join(options.targetDir, "content_scripts/index.tsx"),
|
|
218
|
+
name: options.packageInfo.name,
|
|
219
|
+
formats: ["iife"]
|
|
220
|
+
},
|
|
221
|
+
rollupOptions: { output: {
|
|
222
|
+
entryFileNames: "index.global.js",
|
|
223
|
+
extend: true
|
|
224
|
+
} }
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Copy static extension files (icons, …) into the staging root — the
|
|
229
|
+
* staging dir is build output (gitignored), so committed assets live in
|
|
230
|
+
* the target source tree instead. */
|
|
231
|
+
async function copyWebextStatic(options) {
|
|
232
|
+
const staticDir = options.staticDir ?? path.join(options.targetDir, "static");
|
|
233
|
+
if (!fs.existsSync(staticDir)) return;
|
|
234
|
+
await fs.promises.mkdir(options.stagingDir, { recursive: true });
|
|
235
|
+
await fs.promises.cp(staticDir, options.stagingDir, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
async function writeWebextManifest(options) {
|
|
238
|
+
const manifestPath = path.join(options.stagingDir, "manifest.json");
|
|
239
|
+
await fs.promises.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
240
|
+
await fs.promises.writeFile(manifestPath, JSON.stringify(await options.getManifest(), null, 2), "utf-8");
|
|
241
|
+
}
|
|
242
|
+
/** Dev mode: emit view HTML that loads main.tsx from the vite dev server
|
|
243
|
+
* (extension pages can't be served BY vite — the html on disk must point
|
|
244
|
+
* at it) plus the react-refresh preamble shim. */
|
|
245
|
+
async function stubWebextIndexHtml(options) {
|
|
246
|
+
const port = options.port ?? 3303;
|
|
247
|
+
for (const view of listWebextViews(options.targetDir)) {
|
|
248
|
+
let data = await fs.promises.readFile(path.join(options.targetDir, "views", view, "index.html"), "utf-8");
|
|
249
|
+
data = data.replace("</head>", ` <script type="module" src="http://localhost:${port}/@vite/client"><\/script>
|
|
250
|
+
<script type="module" src="/dist/refresh.js"><\/script>
|
|
251
|
+
</head>`);
|
|
252
|
+
data = data.replace("<script type=\"module\" src=\"./main.tsx\"><\/script>", `<script type="module" src="http://localhost:${port}/views/${view}/main.tsx"><\/script>`).replace("<div id=\"app\"></div>", "<div id=\"app\">vite server did not start</div>");
|
|
253
|
+
const outPath = path.join(options.stagingDir, "dist/views", view, "index.html");
|
|
254
|
+
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
|
255
|
+
await fs.promises.writeFile(outPath, data, "utf-8");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function writeWebextRefreshScript(options) {
|
|
259
|
+
const port = options.port ?? 3303;
|
|
260
|
+
const refreshPath = path.join(options.stagingDir, "dist/refresh.js");
|
|
261
|
+
await fs.promises.mkdir(path.dirname(refreshPath), { recursive: true });
|
|
262
|
+
await fs.promises.writeFile(refreshPath, `window.__vite_plugin_react_preamble_installed__ = true;
|
|
263
|
+
window.$RefreshReg$ = () => {};
|
|
264
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
265
|
+
window.addEventListener('load', () => {
|
|
266
|
+
import("http://localhost:${port}/@react-refresh")
|
|
267
|
+
.then((RefreshRuntime) => {
|
|
268
|
+
if (RefreshRuntime && typeof RefreshRuntime.injectIntoGlobalHook === 'function') {
|
|
269
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
.catch(console.error);
|
|
273
|
+
});
|
|
274
|
+
`, "utf-8");
|
|
275
|
+
}
|
|
276
|
+
/** One-shot (prod) or watching (dev) prepare: manifest + static assets +
|
|
277
|
+
* dev scaffolding. */
|
|
278
|
+
async function runWebextPrepare(options) {
|
|
279
|
+
if (!(options.isDev ?? process.env.NODE_ENV !== "production")) {
|
|
280
|
+
await copyWebextStatic(options);
|
|
281
|
+
await writeWebextManifest(options);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await copyWebextStatic(options);
|
|
285
|
+
await stubWebextIndexHtml(options);
|
|
286
|
+
await writeWebextRefreshScript(options);
|
|
287
|
+
await writeWebextManifest(options);
|
|
288
|
+
if (options.watch) {
|
|
289
|
+
const { watch } = await import("chokidar");
|
|
290
|
+
watch(path.join(options.targetDir, "**/*.html")).on("change", () => stubWebextIndexHtml(options).catch(console.error));
|
|
291
|
+
watch(path.join(options.targetDir, "manifest.ts")).on("change", () => writeWebextManifest(options).catch(console.error));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
export { copyWebextStatic, createWebextBackgroundConfig, createWebextContentConfig, createWebextViewsConfig, listWebextViews, runWebextPrepare, shikiStubPlugin, stubWebextIndexHtml, writeWebextManifest, writeWebextRefreshScript };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@multiplatform.one/vite-plugin-webext",
|
|
3
|
+
"version": "6.2.0",
|
|
4
|
+
"description": "Vite config factories for building a browser web extension (MV3 views, background service worker, content scripts) as a target of a multiplatform.one One app — workspace source aliases, the one-server-only browser stub, Tamagui wiring, and the manifest/dev-reload prepare pipeline.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"browser-extension",
|
|
7
|
+
"multiplatform.one",
|
|
8
|
+
"vite-plugin",
|
|
9
|
+
"webext",
|
|
10
|
+
"webextension"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://multiplatform.one",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one/issues",
|
|
15
|
+
"email": "support@risserlabs.com"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": "BitSpur <support@risserlabs.com> (https://risserlabs.com)",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://gitlab.com/bitspur/frappe/multiplatform.one.git",
|
|
22
|
+
"directory": "public/vite-plugin-webext"
|
|
23
|
+
},
|
|
24
|
+
"source": "src/index.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"types",
|
|
28
|
+
"src",
|
|
29
|
+
"!types/.tsbuildinfo"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "lib/index.cjs",
|
|
34
|
+
"module": "lib/index.mjs",
|
|
35
|
+
"types": "types/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"source": "./src/index.ts",
|
|
39
|
+
"types": "./types/index.d.ts",
|
|
40
|
+
"import": "./lib/index.mjs",
|
|
41
|
+
"require": "./lib/index.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"chokidar": "^5.0.0",
|
|
50
|
+
"@multiplatform.one/utils": "6.2.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@tamagui/vite-plugin": "2.0.0-rc.41",
|
|
54
|
+
"@types/node": "^25.6.0",
|
|
55
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
56
|
+
"typescript": "~5.9.3",
|
|
57
|
+
"vite": "^8.0.10"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@tamagui/vite-plugin": "*",
|
|
61
|
+
"@vitejs/plugin-react": ">=4",
|
|
62
|
+
"vite": ">=5"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"build": "rm -rf lib types 2>/dev/null && tsc -b --emitDeclarationOnly && tsdown"
|
|
67
|
+
}
|
|
68
|
+
}
|