@multiplatform.one/vite-plugin-webext 6.7.0 → 7.1.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/README.md +117 -0
- package/lib/index.cjs +107 -18
- package/lib/index.mjs +103 -13
- package/package.json +3 -3
- package/src/index.ts +160 -12
- package/types/index.d.ts.map +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @multiplatform.one/vite-plugin-webext
|
|
2
|
+
|
|
3
|
+
Vite config factories for building a browser web extension (MV3 views,
|
|
4
|
+
background service worker, content scripts) as a target of a
|
|
5
|
+
multiplatform.one One app. Three vite builds cover the three JS worlds
|
|
6
|
+
(DOM extension pages, worker, injected content script);
|
|
7
|
+
`runWebextPrepare` writes the manifest and the dev-mode HMR scaffolding
|
|
8
|
+
into a loadable staging dir:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
<stagingDir>/manifest.json
|
|
12
|
+
<stagingDir>/dist/views/<view>/index.html (popup/options/sidepanel/…)
|
|
13
|
+
<stagingDir>/dist/background/index.mjs (service worker / bg script)
|
|
14
|
+
<stagingDir>/dist/contentScripts/index.global.js
|
|
15
|
+
<stagingDir>/dist/contentScripts/webext.css (web-accessible resource)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
One config file per JS world, plus a prepare script. Every directory
|
|
21
|
+
under `<targetDir>/views/` with an `index.html` becomes an extension
|
|
22
|
+
page.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// vite.config.webext.ts — extension pages (popup/options/…)
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
import { createWebextViewsConfig } from "@multiplatform.one/vite-plugin-webext";
|
|
28
|
+
import { defineConfig } from "vite";
|
|
29
|
+
import packageJson from "./package.json";
|
|
30
|
+
|
|
31
|
+
export default defineConfig(async () =>
|
|
32
|
+
createWebextViewsConfig({
|
|
33
|
+
targetDir: resolve(import.meta.dirname, "webext"),
|
|
34
|
+
stagingDir: resolve(import.meta.dirname, "dist-webext"),
|
|
35
|
+
tamaguiConfig: resolve(import.meta.dirname, "config/tamagui.config.ts"),
|
|
36
|
+
packageInfo: packageJson,
|
|
37
|
+
// MANDATORY for extension builds — see "one must be aliased" below.
|
|
38
|
+
aliases: [{ find: /^one$/, replacement: "@multiplatform.one/router/seam" }],
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`createWebextBackgroundConfig` (sync) and `createWebextContentConfig`
|
|
44
|
+
(async) take the same options for the worker and content-script builds;
|
|
45
|
+
`runWebextPrepare({ targetDir, stagingDir, getManifest, isDev, port,
|
|
46
|
+
watch })` stages the manifest, static assets, and dev HMR scaffolding.
|
|
47
|
+
|
|
48
|
+
## Required dependencies (the real consumer contract)
|
|
49
|
+
|
|
50
|
+
Declared peers — install them next to this plugin:
|
|
51
|
+
|
|
52
|
+
| package | why |
|
|
53
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
54
|
+
| `vite` >=5 | the build tool itself |
|
|
55
|
+
| `@vitejs/plugin-react` | views build (react fast-refresh + jsx). Lazy-imported at factory call |
|
|
56
|
+
| `@tamagui/vite-plugin` | views + content builds (static extraction). Lazy-imported |
|
|
57
|
+
|
|
58
|
+
Beyond the declared peers, a real app build needs:
|
|
59
|
+
|
|
60
|
+
- **`@tamagui/web` as a direct dependency of your app.** The Tamagui
|
|
61
|
+
static extractor bundles your `tamaguiConfig` to
|
|
62
|
+
`<cwd>/.tamagui/tamagui.config.{cjs,mjs}` and requires it from there,
|
|
63
|
+
with `@tamagui/web` left external. Under pnpm's default isolated
|
|
64
|
+
`node_modules`, transitive packages are not resolvable from your app,
|
|
65
|
+
so the extractor fails with "Error bundling tamagui config: Cannot
|
|
66
|
+
find package '@tamagui/web'…" — and (upstream) swallows it, silently
|
|
67
|
+
shipping a deoptimized runtime-styled build. This plugin turns that
|
|
68
|
+
into a hard error in production builds; fix it by adding
|
|
69
|
+
`@tamagui/web` to your app's `dependencies`.
|
|
70
|
+
- **`react-i18next` + `i18next`, if you render the i18n-using catalog
|
|
71
|
+
components.** `@multiplatform.one/components` translates built-in
|
|
72
|
+
copy (Alert, Toast, Carousel, ConfirmDialog, Pagination, views/,
|
|
73
|
+
layouts/, …) via an optional `react-i18next` peer. Apps that never
|
|
74
|
+
render those components may omit both packages and tree-shake the
|
|
75
|
+
components away; apps that do render them get a named runtime error
|
|
76
|
+
telling you to install `react-i18next` and `i18next`.
|
|
77
|
+
|
|
78
|
+
## `one` must be aliased to the router seam
|
|
79
|
+
|
|
80
|
+
Extension pages must NOT bundle the real `one` runtime: it drags in
|
|
81
|
+
`@react-navigation/core` (and server-only modules) and the build fails.
|
|
82
|
+
This is mandatory, not stylistic — alias `one` to the stack-navigation
|
|
83
|
+
seam in every webext config that renders shared feature code:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
aliases: [{ find: /^one$/, replacement: "@multiplatform.one/router/seam" }],
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The seam (`@multiplatform.one/router/seam`) implements One's router API
|
|
90
|
+
over DOM pages, so shared `features/` code written against One's router
|
|
91
|
+
works unchanged inside the extension.
|
|
92
|
+
|
|
93
|
+
Relatedly, `one`'s client code imports `./vite/one-server-only.mjs`
|
|
94
|
+
(node-only); the factories redirect any `one-server-only` id to one's
|
|
95
|
+
published browser/native no-op. The redirect resolves the installed
|
|
96
|
+
`one` package through node resolution (build cwd, then
|
|
97
|
+
`workspaceRoot`); when `one` is not installed at all the redirect is
|
|
98
|
+
skipped with a one-line warning and normal resolution proceeds.
|
|
99
|
+
|
|
100
|
+
## `workspaceRoot` semantics
|
|
101
|
+
|
|
102
|
+
`workspaceRoot` defaults to the git toplevel (`git rev-parse
|
|
103
|
+
--show-toplevel`) of the build cwd. It is used for two things:
|
|
104
|
+
|
|
105
|
+
1. **Workspace source aliases.** Inside the multiplatform.one monorepo,
|
|
106
|
+
every `public/*` package (and its subpath exports) is aliased to its
|
|
107
|
+
TypeScript source so builds never consume stale `dist` artifacts.
|
|
108
|
+
Out-of-repo there is no `public/` under the root, the aliasing
|
|
109
|
+
no-ops, and `@multiplatform.one/*` resolves from `node_modules` like
|
|
110
|
+
any other package — the factories log one line when this happens:
|
|
111
|
+
`no public/ under <root> — resolving @multiplatform.one/* from
|
|
112
|
+
node_modules`.
|
|
113
|
+
2. **The one-server-only redirect** falls back to resolving `one` from
|
|
114
|
+
`workspaceRoot` when it cannot be resolved from the build cwd.
|
|
115
|
+
|
|
116
|
+
Pass `workspaceRoot` explicitly when your app builds from a directory
|
|
117
|
+
whose git toplevel is not the dependency root (e.g. a nested repo).
|
package/lib/index.cjs
CHANGED
|
@@ -28,15 +28,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
//#endregion
|
|
29
29
|
let node_fs = require("node:fs");
|
|
30
30
|
node_fs = __toESM(node_fs, 1);
|
|
31
|
+
let node_module = require("node:module");
|
|
31
32
|
let node_path = require("node:path");
|
|
32
33
|
node_path = __toESM(node_path, 1);
|
|
33
34
|
let _multiplatform_one_utils_dev = require("@multiplatform.one/utils/dev");
|
|
34
35
|
let _rolldown_plugin_babel = require("@rolldown/plugin-babel");
|
|
35
36
|
_rolldown_plugin_babel = __toESM(_rolldown_plugin_babel, 1);
|
|
36
|
-
let _vitejs_plugin_react = require("@vitejs/plugin-react");
|
|
37
|
-
_vitejs_plugin_react = __toESM(_vitejs_plugin_react, 1);
|
|
38
37
|
|
|
39
38
|
//#region src/index.ts
|
|
39
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
40
|
+
const logger = console;
|
|
40
41
|
const WEBEXT_EXTENSIONS = [
|
|
41
42
|
".webext.ts",
|
|
42
43
|
".webext.tsx",
|
|
@@ -78,13 +79,91 @@ function shikiStubPlugin() {
|
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
}
|
|
82
|
+
function barePackageName(specifier) {
|
|
83
|
+
if (!specifier || /^[./#\\]/.test(specifier)) return;
|
|
84
|
+
if (/^(node|data|file|https?):/.test(specifier)) return;
|
|
85
|
+
const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
86
|
+
if (node_module.builtinModules.includes(packageName)) return;
|
|
87
|
+
return packageName;
|
|
88
|
+
}
|
|
89
|
+
/** Node ESM bare-import resolution walks parent node_modules dirs and — unlike
|
|
90
|
+
* CJS — ignores NODE_PATH. Presence of the package dir is what we probe. */
|
|
91
|
+
function packageVisibleFromDir(packageName, fromDir) {
|
|
92
|
+
let dir = fromDir;
|
|
93
|
+
while (true) {
|
|
94
|
+
if (node_fs.default.existsSync(node_path.default.join(dir, "node_modules", packageName, "package.json"))) return true;
|
|
95
|
+
const parent = node_path.default.dirname(dir);
|
|
96
|
+
if (parent === dir) return false;
|
|
97
|
+
dir = parent;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The Tamagui static extractor bundles the app's tamagui config to
|
|
102
|
+
* `<cwd>/.tamagui/tamagui.config.{cjs,mjs}` and then loads it (ESM `import()`
|
|
103
|
+
* for .mjs, `require()` for .cjs), leaving its externals (`@tamagui/web`,
|
|
104
|
+
* `@tamagui/core`, react…) as live imports resolved FROM that artifact. When
|
|
105
|
+
* one of them cannot be resolved there (pnpm's default isolated layout
|
|
106
|
+
* without a direct `@tamagui/web` dependency), the extractor logs "Error
|
|
107
|
+
* bundling tamagui config: Cannot find package …", swallows the error, and
|
|
108
|
+
* the build exits 0 with a silently deoptimized runtime-styled bundle.
|
|
109
|
+
* Production builds must fail instead: after the build, replay the artifact's
|
|
110
|
+
* imports with the loader semantics the extractor would use — CJS resolution
|
|
111
|
+
* for .cjs (honors NODE_PATH, which pnpm bin shims set), an ESM-style
|
|
112
|
+
* node_modules walk for .mjs (ESM ignores NODE_PATH — exactly the divergence
|
|
113
|
+
* that breaks the .mjs artifact under pnpm) — and name the missing package.
|
|
114
|
+
* A missing artifact (bundling died before writing it) is equally fatal.
|
|
115
|
+
*/
|
|
116
|
+
function tamaguiConfigBundleGuard(isDev) {
|
|
117
|
+
return {
|
|
118
|
+
name: "webext-tamagui-config-guard",
|
|
119
|
+
closeBundle() {
|
|
120
|
+
if (isDev) return;
|
|
121
|
+
const dotDir = node_path.default.join(process.cwd(), ".tamagui");
|
|
122
|
+
const artifacts = ["tamagui.config.cjs", "tamagui.config.mjs"].map((name) => node_path.default.join(dotDir, name)).filter((file) => node_fs.default.existsSync(file));
|
|
123
|
+
if (artifacts.length === 0) {
|
|
124
|
+
if (!node_fs.default.existsSync(dotDir)) return;
|
|
125
|
+
throw new Error(`[${PLUGIN_NAME}] the Tamagui static extractor did not produce its bundled config (expected ${node_path.default.join(dotDir, "tamagui.config.{cjs,mjs}")}). The build would silently fall back to a deoptimized runtime-styled bundle. Re-run with DEBUG=tamagui to see the underlying bundling error.`);
|
|
126
|
+
}
|
|
127
|
+
const artifact = artifacts.sort((a, b) => node_fs.default.statSync(b).mtimeMs - node_fs.default.statSync(a).mtimeMs)[0];
|
|
128
|
+
const source = node_fs.default.readFileSync(artifact, "utf8");
|
|
129
|
+
const requireFromArtifact = (0, node_module.createRequire)(artifact);
|
|
130
|
+
const importRe = /(?:require\s*\(\s*|import\s*\(\s*|from\s*|import\s*)["']([^"'\n]+)["']/g;
|
|
131
|
+
const missing = /* @__PURE__ */ new Set();
|
|
132
|
+
for (const match of source.matchAll(importRe)) {
|
|
133
|
+
const packageName = barePackageName(match[1]);
|
|
134
|
+
if (!packageName) continue;
|
|
135
|
+
if (artifact.endsWith(".cjs")) try {
|
|
136
|
+
requireFromArtifact.resolve(packageName);
|
|
137
|
+
} catch {
|
|
138
|
+
missing.add(packageName);
|
|
139
|
+
}
|
|
140
|
+
else if (!packageVisibleFromDir(packageName, dotDir)) missing.add(packageName);
|
|
141
|
+
}
|
|
142
|
+
if (missing.size > 0) {
|
|
143
|
+
const names = [...missing].map((name) => `"${name}"`).join(", ");
|
|
144
|
+
throw new Error(`[${PLUGIN_NAME}] Tamagui config bundling failed: ${names} cannot be resolved from ${artifact}. The Tamagui static extractor swallows this ("Error bundling tamagui config") and ships a deoptimized runtime-styled build. Fix: add ${names} to your app's dependencies — under pnpm's default isolated node_modules, transitive packages are not resolvable from the app.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
81
149
|
function resolveDefaults(options) {
|
|
150
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
82
151
|
return {
|
|
83
152
|
workspaceRoot: options.workspaceRoot ?? (0, _multiplatform_one_utils_dev.lookupProjectRoot)(),
|
|
153
|
+
workspaceRootInferred,
|
|
84
154
|
isDev: options.isDev ?? process.env.NODE_ENV !== "production",
|
|
85
155
|
port: options.port ?? (Number(process.env.PORT) || 3303)
|
|
86
156
|
};
|
|
87
157
|
}
|
|
158
|
+
/** Peers stay lazy so requiring them is deferred to the factory that needs
|
|
159
|
+
* them — but a missing peer must name itself, not crash the config file. */
|
|
160
|
+
async function importPeer(load, specifier) {
|
|
161
|
+
try {
|
|
162
|
+
return await load();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new Error(`[${PLUGIN_NAME}] could not import its peer dependency "${specifier}" — install it next to ${PLUGIN_NAME} (it is a required peer): ${err instanceof Error ? err.message : err}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
88
167
|
function baseDefine(options, isDev) {
|
|
89
168
|
return {
|
|
90
169
|
__DEV__: isDev,
|
|
@@ -94,10 +173,16 @@ function baseDefine(options, isDev) {
|
|
|
94
173
|
...options.define
|
|
95
174
|
};
|
|
96
175
|
}
|
|
97
|
-
|
|
176
|
+
const loggedNoPublicRoots = /* @__PURE__ */ new Set();
|
|
177
|
+
function baseResolve(options, workspaceRoot, workspaceRootInferred) {
|
|
178
|
+
const sourceAliases = (0, _multiplatform_one_utils_dev.workspaceSourceAliases)(workspaceRoot);
|
|
179
|
+
if (workspaceRootInferred && Object.keys(sourceAliases).length === 0 && !loggedNoPublicRoots.has(workspaceRoot)) {
|
|
180
|
+
loggedNoPublicRoots.add(workspaceRoot);
|
|
181
|
+
logger.info(`[${PLUGIN_NAME}] no public/ under ${workspaceRoot} — resolving @multiplatform.one/* from node_modules`);
|
|
182
|
+
}
|
|
98
183
|
return {
|
|
99
184
|
alias: [
|
|
100
|
-
...Object.entries(
|
|
185
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
101
186
|
find,
|
|
102
187
|
replacement
|
|
103
188
|
})),
|
|
@@ -137,8 +222,9 @@ function listWebextViews(targetDir) {
|
|
|
137
222
|
}
|
|
138
223
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
139
224
|
async function createWebextViewsConfig(options) {
|
|
140
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
141
|
-
const {
|
|
225
|
+
const { workspaceRoot, workspaceRootInferred, isDev, port } = resolveDefaults(options);
|
|
226
|
+
const { default: react, reactCompilerPreset } = await importPeer(() => import("@vitejs/plugin-react"), "@vitejs/plugin-react");
|
|
227
|
+
const { tamaguiPlugin } = await importPeer(() => import("@tamagui/vite-plugin"), "@tamagui/vite-plugin");
|
|
142
228
|
const views = listWebextViews(options.targetDir);
|
|
143
229
|
return {
|
|
144
230
|
root: options.targetDir,
|
|
@@ -146,13 +232,14 @@ async function createWebextViewsConfig(options) {
|
|
|
146
232
|
plugins: [
|
|
147
233
|
(0, _multiplatform_one_utils_dev.stubExpoTypeDeclarations)(),
|
|
148
234
|
(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot),
|
|
149
|
-
(
|
|
150
|
-
(0, _rolldown_plugin_babel.default)({ presets: [
|
|
235
|
+
react({ jsxRuntime: "automatic" }),
|
|
236
|
+
(0, _rolldown_plugin_babel.default)({ presets: [reactCompilerPreset()] }),
|
|
151
237
|
tamaguiPlugin({
|
|
152
238
|
components: (0, _multiplatform_one_utils_dev.lookupTamaguiModules)([options.targetDir]),
|
|
153
239
|
config: options.tamaguiConfig,
|
|
154
240
|
outputCSS: node_path.default.join(options.targetDir, "tamagui.css")
|
|
155
|
-
})
|
|
241
|
+
}),
|
|
242
|
+
tamaguiConfigBundleGuard(isDev)
|
|
156
243
|
],
|
|
157
244
|
define: baseDefine(options, isDev),
|
|
158
245
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -160,7 +247,7 @@ async function createWebextViewsConfig(options) {
|
|
|
160
247
|
jsx: "automatic",
|
|
161
248
|
target: "esnext"
|
|
162
249
|
},
|
|
163
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
250
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
164
251
|
server: {
|
|
165
252
|
port,
|
|
166
253
|
hmr: {
|
|
@@ -185,7 +272,7 @@ async function createWebextViewsConfig(options) {
|
|
|
185
272
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
186
273
|
* build — a single-file iife, no DOM. */
|
|
187
274
|
function createWebextBackgroundConfig(options) {
|
|
188
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
275
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
189
276
|
return {
|
|
190
277
|
root: options.targetDir,
|
|
191
278
|
plugins: [(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot)],
|
|
@@ -195,7 +282,7 @@ function createWebextBackgroundConfig(options) {
|
|
|
195
282
|
jsx: "automatic",
|
|
196
283
|
target: "esnext"
|
|
197
284
|
},
|
|
198
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
285
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
199
286
|
build: {
|
|
200
287
|
watch: isDev ? {} : void 0,
|
|
201
288
|
outDir: node_path.default.join(options.stagingDir, "dist/background"),
|
|
@@ -217,21 +304,23 @@ function createWebextBackgroundConfig(options) {
|
|
|
217
304
|
/** The content-script build — a single-file iife injected into pages, with
|
|
218
305
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
219
306
|
async function createWebextContentConfig(options) {
|
|
220
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
221
|
-
const {
|
|
307
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
308
|
+
const { default: react, reactCompilerPreset } = await importPeer(() => import("@vitejs/plugin-react"), "@vitejs/plugin-react");
|
|
309
|
+
const { tamaguiPlugin } = await importPeer(() => import("@tamagui/vite-plugin"), "@tamagui/vite-plugin");
|
|
222
310
|
return {
|
|
223
311
|
root: options.targetDir,
|
|
224
312
|
plugins: [
|
|
225
313
|
(0, _multiplatform_one_utils_dev.stubExpoTypeDeclarations)(),
|
|
226
314
|
(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot),
|
|
227
315
|
shikiStubPlugin(),
|
|
228
|
-
(
|
|
229
|
-
(0, _rolldown_plugin_babel.default)({ presets: [
|
|
316
|
+
react({ jsxRuntime: "automatic" }),
|
|
317
|
+
(0, _rolldown_plugin_babel.default)({ presets: [reactCompilerPreset()] }),
|
|
230
318
|
tamaguiPlugin({
|
|
231
319
|
components: (0, _multiplatform_one_utils_dev.lookupTamaguiModules)([options.targetDir]),
|
|
232
320
|
config: options.tamaguiConfig,
|
|
233
321
|
outputCSS: node_path.default.join(options.targetDir, "tamagui.content.css")
|
|
234
|
-
})
|
|
322
|
+
}),
|
|
323
|
+
tamaguiConfigBundleGuard(isDev)
|
|
235
324
|
],
|
|
236
325
|
define: {
|
|
237
326
|
...baseDefine(options, isDev),
|
|
@@ -242,7 +331,7 @@ async function createWebextContentConfig(options) {
|
|
|
242
331
|
jsx: "automatic",
|
|
243
332
|
target: "esnext"
|
|
244
333
|
},
|
|
245
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
334
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
246
335
|
build: {
|
|
247
336
|
watch: isDev ? {} : void 0,
|
|
248
337
|
outDir: node_path.default.join(options.stagingDir, "dist/contentScripts"),
|
package/lib/index.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { builtinModules, createRequire } from "node:module";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { lookupProjectRoot, lookupTamaguiModules, oneServerOnlyBrowserStub, stubExpoTypeDeclarations, workspaceSourceAliases } from "@multiplatform.one/utils/dev";
|
|
4
5
|
import babel from "@rolldown/plugin-babel";
|
|
5
|
-
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
6
6
|
|
|
7
7
|
//#region src/index.ts
|
|
8
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
9
|
+
const logger = console;
|
|
8
10
|
const WEBEXT_EXTENSIONS = [
|
|
9
11
|
".webext.ts",
|
|
10
12
|
".webext.tsx",
|
|
@@ -46,13 +48,91 @@ function shikiStubPlugin() {
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
}
|
|
51
|
+
function barePackageName(specifier) {
|
|
52
|
+
if (!specifier || /^[./#\\]/.test(specifier)) return;
|
|
53
|
+
if (/^(node|data|file|https?):/.test(specifier)) return;
|
|
54
|
+
const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
55
|
+
if (builtinModules.includes(packageName)) return;
|
|
56
|
+
return packageName;
|
|
57
|
+
}
|
|
58
|
+
/** Node ESM bare-import resolution walks parent node_modules dirs and — unlike
|
|
59
|
+
* CJS — ignores NODE_PATH. Presence of the package dir is what we probe. */
|
|
60
|
+
function packageVisibleFromDir(packageName, fromDir) {
|
|
61
|
+
let dir = fromDir;
|
|
62
|
+
while (true) {
|
|
63
|
+
if (fs.existsSync(path.join(dir, "node_modules", packageName, "package.json"))) return true;
|
|
64
|
+
const parent = path.dirname(dir);
|
|
65
|
+
if (parent === dir) return false;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The Tamagui static extractor bundles the app's tamagui config to
|
|
71
|
+
* `<cwd>/.tamagui/tamagui.config.{cjs,mjs}` and then loads it (ESM `import()`
|
|
72
|
+
* for .mjs, `require()` for .cjs), leaving its externals (`@tamagui/web`,
|
|
73
|
+
* `@tamagui/core`, react…) as live imports resolved FROM that artifact. When
|
|
74
|
+
* one of them cannot be resolved there (pnpm's default isolated layout
|
|
75
|
+
* without a direct `@tamagui/web` dependency), the extractor logs "Error
|
|
76
|
+
* bundling tamagui config: Cannot find package …", swallows the error, and
|
|
77
|
+
* the build exits 0 with a silently deoptimized runtime-styled bundle.
|
|
78
|
+
* Production builds must fail instead: after the build, replay the artifact's
|
|
79
|
+
* imports with the loader semantics the extractor would use — CJS resolution
|
|
80
|
+
* for .cjs (honors NODE_PATH, which pnpm bin shims set), an ESM-style
|
|
81
|
+
* node_modules walk for .mjs (ESM ignores NODE_PATH — exactly the divergence
|
|
82
|
+
* that breaks the .mjs artifact under pnpm) — and name the missing package.
|
|
83
|
+
* A missing artifact (bundling died before writing it) is equally fatal.
|
|
84
|
+
*/
|
|
85
|
+
function tamaguiConfigBundleGuard(isDev) {
|
|
86
|
+
return {
|
|
87
|
+
name: "webext-tamagui-config-guard",
|
|
88
|
+
closeBundle() {
|
|
89
|
+
if (isDev) return;
|
|
90
|
+
const dotDir = path.join(process.cwd(), ".tamagui");
|
|
91
|
+
const artifacts = ["tamagui.config.cjs", "tamagui.config.mjs"].map((name) => path.join(dotDir, name)).filter((file) => fs.existsSync(file));
|
|
92
|
+
if (artifacts.length === 0) {
|
|
93
|
+
if (!fs.existsSync(dotDir)) return;
|
|
94
|
+
throw new Error(`[${PLUGIN_NAME}] the Tamagui static extractor did not produce its bundled config (expected ${path.join(dotDir, "tamagui.config.{cjs,mjs}")}). The build would silently fall back to a deoptimized runtime-styled bundle. Re-run with DEBUG=tamagui to see the underlying bundling error.`);
|
|
95
|
+
}
|
|
96
|
+
const artifact = artifacts.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
97
|
+
const source = fs.readFileSync(artifact, "utf8");
|
|
98
|
+
const requireFromArtifact = createRequire(artifact);
|
|
99
|
+
const importRe = /(?:require\s*\(\s*|import\s*\(\s*|from\s*|import\s*)["']([^"'\n]+)["']/g;
|
|
100
|
+
const missing = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const match of source.matchAll(importRe)) {
|
|
102
|
+
const packageName = barePackageName(match[1]);
|
|
103
|
+
if (!packageName) continue;
|
|
104
|
+
if (artifact.endsWith(".cjs")) try {
|
|
105
|
+
requireFromArtifact.resolve(packageName);
|
|
106
|
+
} catch {
|
|
107
|
+
missing.add(packageName);
|
|
108
|
+
}
|
|
109
|
+
else if (!packageVisibleFromDir(packageName, dotDir)) missing.add(packageName);
|
|
110
|
+
}
|
|
111
|
+
if (missing.size > 0) {
|
|
112
|
+
const names = [...missing].map((name) => `"${name}"`).join(", ");
|
|
113
|
+
throw new Error(`[${PLUGIN_NAME}] Tamagui config bundling failed: ${names} cannot be resolved from ${artifact}. The Tamagui static extractor swallows this ("Error bundling tamagui config") and ships a deoptimized runtime-styled build. Fix: add ${names} to your app's dependencies — under pnpm's default isolated node_modules, transitive packages are not resolvable from the app.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
49
118
|
function resolveDefaults(options) {
|
|
119
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
50
120
|
return {
|
|
51
121
|
workspaceRoot: options.workspaceRoot ?? lookupProjectRoot(),
|
|
122
|
+
workspaceRootInferred,
|
|
52
123
|
isDev: options.isDev ?? process.env.NODE_ENV !== "production",
|
|
53
124
|
port: options.port ?? (Number(process.env.PORT) || 3303)
|
|
54
125
|
};
|
|
55
126
|
}
|
|
127
|
+
/** Peers stay lazy so requiring them is deferred to the factory that needs
|
|
128
|
+
* them — but a missing peer must name itself, not crash the config file. */
|
|
129
|
+
async function importPeer(load, specifier) {
|
|
130
|
+
try {
|
|
131
|
+
return await load();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw new Error(`[${PLUGIN_NAME}] could not import its peer dependency "${specifier}" — install it next to ${PLUGIN_NAME} (it is a required peer): ${err instanceof Error ? err.message : err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
56
136
|
function baseDefine(options, isDev) {
|
|
57
137
|
return {
|
|
58
138
|
__DEV__: isDev,
|
|
@@ -62,10 +142,16 @@ function baseDefine(options, isDev) {
|
|
|
62
142
|
...options.define
|
|
63
143
|
};
|
|
64
144
|
}
|
|
65
|
-
|
|
145
|
+
const loggedNoPublicRoots = /* @__PURE__ */ new Set();
|
|
146
|
+
function baseResolve(options, workspaceRoot, workspaceRootInferred) {
|
|
147
|
+
const sourceAliases = workspaceSourceAliases(workspaceRoot);
|
|
148
|
+
if (workspaceRootInferred && Object.keys(sourceAliases).length === 0 && !loggedNoPublicRoots.has(workspaceRoot)) {
|
|
149
|
+
loggedNoPublicRoots.add(workspaceRoot);
|
|
150
|
+
logger.info(`[${PLUGIN_NAME}] no public/ under ${workspaceRoot} — resolving @multiplatform.one/* from node_modules`);
|
|
151
|
+
}
|
|
66
152
|
return {
|
|
67
153
|
alias: [
|
|
68
|
-
...Object.entries(
|
|
154
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
69
155
|
find,
|
|
70
156
|
replacement
|
|
71
157
|
})),
|
|
@@ -105,8 +191,9 @@ function listWebextViews(targetDir) {
|
|
|
105
191
|
}
|
|
106
192
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
107
193
|
async function createWebextViewsConfig(options) {
|
|
108
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
109
|
-
const {
|
|
194
|
+
const { workspaceRoot, workspaceRootInferred, isDev, port } = resolveDefaults(options);
|
|
195
|
+
const { default: react, reactCompilerPreset } = await importPeer(() => import("@vitejs/plugin-react"), "@vitejs/plugin-react");
|
|
196
|
+
const { tamaguiPlugin } = await importPeer(() => import("@tamagui/vite-plugin"), "@tamagui/vite-plugin");
|
|
110
197
|
const views = listWebextViews(options.targetDir);
|
|
111
198
|
return {
|
|
112
199
|
root: options.targetDir,
|
|
@@ -120,7 +207,8 @@ async function createWebextViewsConfig(options) {
|
|
|
120
207
|
components: lookupTamaguiModules([options.targetDir]),
|
|
121
208
|
config: options.tamaguiConfig,
|
|
122
209
|
outputCSS: path.join(options.targetDir, "tamagui.css")
|
|
123
|
-
})
|
|
210
|
+
}),
|
|
211
|
+
tamaguiConfigBundleGuard(isDev)
|
|
124
212
|
],
|
|
125
213
|
define: baseDefine(options, isDev),
|
|
126
214
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -128,7 +216,7 @@ async function createWebextViewsConfig(options) {
|
|
|
128
216
|
jsx: "automatic",
|
|
129
217
|
target: "esnext"
|
|
130
218
|
},
|
|
131
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
219
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
132
220
|
server: {
|
|
133
221
|
port,
|
|
134
222
|
hmr: {
|
|
@@ -153,7 +241,7 @@ async function createWebextViewsConfig(options) {
|
|
|
153
241
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
154
242
|
* build — a single-file iife, no DOM. */
|
|
155
243
|
function createWebextBackgroundConfig(options) {
|
|
156
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
244
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
157
245
|
return {
|
|
158
246
|
root: options.targetDir,
|
|
159
247
|
plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
|
|
@@ -163,7 +251,7 @@ function createWebextBackgroundConfig(options) {
|
|
|
163
251
|
jsx: "automatic",
|
|
164
252
|
target: "esnext"
|
|
165
253
|
},
|
|
166
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
254
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
167
255
|
build: {
|
|
168
256
|
watch: isDev ? {} : void 0,
|
|
169
257
|
outDir: path.join(options.stagingDir, "dist/background"),
|
|
@@ -185,8 +273,9 @@ function createWebextBackgroundConfig(options) {
|
|
|
185
273
|
/** The content-script build — a single-file iife injected into pages, with
|
|
186
274
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
187
275
|
async function createWebextContentConfig(options) {
|
|
188
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
189
|
-
const {
|
|
276
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
277
|
+
const { default: react, reactCompilerPreset } = await importPeer(() => import("@vitejs/plugin-react"), "@vitejs/plugin-react");
|
|
278
|
+
const { tamaguiPlugin } = await importPeer(() => import("@tamagui/vite-plugin"), "@tamagui/vite-plugin");
|
|
190
279
|
return {
|
|
191
280
|
root: options.targetDir,
|
|
192
281
|
plugins: [
|
|
@@ -199,7 +288,8 @@ async function createWebextContentConfig(options) {
|
|
|
199
288
|
components: lookupTamaguiModules([options.targetDir]),
|
|
200
289
|
config: options.tamaguiConfig,
|
|
201
290
|
outputCSS: path.join(options.targetDir, "tamagui.content.css")
|
|
202
|
-
})
|
|
291
|
+
}),
|
|
292
|
+
tamaguiConfigBundleGuard(isDev)
|
|
203
293
|
],
|
|
204
294
|
define: {
|
|
205
295
|
...baseDefine(options, isDev),
|
|
@@ -210,7 +300,7 @@ async function createWebextContentConfig(options) {
|
|
|
210
300
|
jsx: "automatic",
|
|
211
301
|
target: "esnext"
|
|
212
302
|
},
|
|
213
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
303
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
214
304
|
build: {
|
|
215
305
|
watch: isDev ? {} : void 0,
|
|
216
306
|
outDir: path.join(options.stagingDir, "dist/contentScripts"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/vite-plugin-webext",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"browser-extension",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"@rolldown/plugin-babel": "^0.2.3",
|
|
51
51
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
52
52
|
"chokidar": "^5.0.0",
|
|
53
|
-
"@multiplatform.one/utils": "
|
|
53
|
+
"@multiplatform.one/utils": "7.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@tamagui/vite-plugin": "2.
|
|
56
|
+
"@tamagui/vite-plugin": "2.7.6",
|
|
57
57
|
"@types/babel__core": "^7.20.5",
|
|
58
58
|
"@types/node": "^25.6.0",
|
|
59
59
|
"@vitejs/plugin-react": "^6.0.1",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// @multiplatform.one/utils/dev so every product app shares one copy.
|
|
18
18
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
|
+
import { builtinModules, createRequire } from "node:module";
|
|
20
21
|
import path from "node:path";
|
|
21
22
|
import {
|
|
22
23
|
lookupTamaguiModules,
|
|
@@ -26,9 +27,12 @@ import {
|
|
|
26
27
|
lookupProjectRoot,
|
|
27
28
|
} from "@multiplatform.one/utils/dev";
|
|
28
29
|
import babel from "@rolldown/plugin-babel";
|
|
29
|
-
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
30
30
|
import type { Alias, Plugin, UserConfig } from "vite";
|
|
31
31
|
|
|
32
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
33
|
+
|
|
34
|
+
const logger = console;
|
|
35
|
+
|
|
32
36
|
export interface WebextPackageInfo {
|
|
33
37
|
name: string;
|
|
34
38
|
displayName?: string;
|
|
@@ -104,11 +108,118 @@ export function shikiStubPlugin(): Plugin {
|
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
function barePackageName(specifier: string): string | undefined {
|
|
112
|
+
if (!specifier || /^[./#\\]/.test(specifier)) return;
|
|
113
|
+
if (/^(node|data|file|https?):/.test(specifier)) return;
|
|
114
|
+
const packageName = specifier.startsWith("@")
|
|
115
|
+
? specifier.split("/").slice(0, 2).join("/")
|
|
116
|
+
: specifier.split("/")[0]!;
|
|
117
|
+
if (builtinModules.includes(packageName)) return;
|
|
118
|
+
return packageName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Node ESM bare-import resolution walks parent node_modules dirs and — unlike
|
|
122
|
+
* CJS — ignores NODE_PATH. Presence of the package dir is what we probe. */
|
|
123
|
+
function packageVisibleFromDir(packageName: string, fromDir: string): boolean {
|
|
124
|
+
let dir = fromDir;
|
|
125
|
+
while (true) {
|
|
126
|
+
if (fs.existsSync(path.join(dir, "node_modules", packageName, "package.json"))) return true;
|
|
127
|
+
const parent = path.dirname(dir);
|
|
128
|
+
if (parent === dir) return false;
|
|
129
|
+
dir = parent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The Tamagui static extractor bundles the app's tamagui config to
|
|
135
|
+
* `<cwd>/.tamagui/tamagui.config.{cjs,mjs}` and then loads it (ESM `import()`
|
|
136
|
+
* for .mjs, `require()` for .cjs), leaving its externals (`@tamagui/web`,
|
|
137
|
+
* `@tamagui/core`, react…) as live imports resolved FROM that artifact. When
|
|
138
|
+
* one of them cannot be resolved there (pnpm's default isolated layout
|
|
139
|
+
* without a direct `@tamagui/web` dependency), the extractor logs "Error
|
|
140
|
+
* bundling tamagui config: Cannot find package …", swallows the error, and
|
|
141
|
+
* the build exits 0 with a silently deoptimized runtime-styled bundle.
|
|
142
|
+
* Production builds must fail instead: after the build, replay the artifact's
|
|
143
|
+
* imports with the loader semantics the extractor would use — CJS resolution
|
|
144
|
+
* for .cjs (honors NODE_PATH, which pnpm bin shims set), an ESM-style
|
|
145
|
+
* node_modules walk for .mjs (ESM ignores NODE_PATH — exactly the divergence
|
|
146
|
+
* that breaks the .mjs artifact under pnpm) — and name the missing package.
|
|
147
|
+
* A missing artifact (bundling died before writing it) is equally fatal.
|
|
148
|
+
*/
|
|
149
|
+
function tamaguiConfigBundleGuard(isDev: boolean): Plugin {
|
|
150
|
+
return {
|
|
151
|
+
name: "webext-tamagui-config-guard",
|
|
152
|
+
closeBundle() {
|
|
153
|
+
if (isDev) return;
|
|
154
|
+
const dotDir = path.join(process.cwd(), ".tamagui");
|
|
155
|
+
const artifacts = ["tamagui.config.cjs", "tamagui.config.mjs"]
|
|
156
|
+
.map((name) => path.join(dotDir, name))
|
|
157
|
+
.filter((file) => fs.existsSync(file));
|
|
158
|
+
if (artifacts.length === 0) {
|
|
159
|
+
// No .tamagui dir at all means the extractor never ran for this
|
|
160
|
+
// build (nothing to verify) — an existing dir without the config
|
|
161
|
+
// artifact means bundling died before writing it.
|
|
162
|
+
if (!fs.existsSync(dotDir)) return;
|
|
163
|
+
throw new Error(
|
|
164
|
+
`[${PLUGIN_NAME}] the Tamagui static extractor did not produce its bundled config ` +
|
|
165
|
+
`(expected ${path.join(dotDir, "tamagui.config.{cjs,mjs}")}). The build would ` +
|
|
166
|
+
"silently fall back to a deoptimized runtime-styled bundle. Re-run with " +
|
|
167
|
+
"DEBUG=tamagui to see the underlying bundling error.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const artifact = artifacts.sort(
|
|
171
|
+
(a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs,
|
|
172
|
+
)[0]!;
|
|
173
|
+
const source = fs.readFileSync(artifact, "utf8");
|
|
174
|
+
const requireFromArtifact = createRequire(artifact);
|
|
175
|
+
const importRe = /(?:require\s*\(\s*|import\s*\(\s*|from\s*|import\s*)["']([^"'\n]+)["']/g;
|
|
176
|
+
const missing = new Set<string>();
|
|
177
|
+
for (const match of source.matchAll(importRe)) {
|
|
178
|
+
const packageName = barePackageName(match[1]!);
|
|
179
|
+
if (!packageName) continue;
|
|
180
|
+
if (artifact.endsWith(".cjs")) {
|
|
181
|
+
try {
|
|
182
|
+
requireFromArtifact.resolve(packageName);
|
|
183
|
+
} catch {
|
|
184
|
+
missing.add(packageName);
|
|
185
|
+
}
|
|
186
|
+
} else if (!packageVisibleFromDir(packageName, dotDir)) {
|
|
187
|
+
missing.add(packageName);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (missing.size > 0) {
|
|
191
|
+
const names = [...missing].map((name) => `"${name}"`).join(", ");
|
|
192
|
+
throw new Error(
|
|
193
|
+
`[${PLUGIN_NAME}] Tamagui config bundling failed: ${names} cannot be resolved ` +
|
|
194
|
+
`from ${artifact}. The Tamagui static extractor swallows this ("Error bundling ` +
|
|
195
|
+
'tamagui config") and ships a deoptimized runtime-styled build. Fix: add ' +
|
|
196
|
+
`${names} to your app's dependencies — under pnpm's default isolated ` +
|
|
197
|
+
"node_modules, transitive packages are not resolvable from the app.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
107
204
|
function resolveDefaults(options: WebextTargetOptions) {
|
|
205
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
108
206
|
const workspaceRoot = options.workspaceRoot ?? lookupProjectRoot();
|
|
109
207
|
const isDev = options.isDev ?? process.env.NODE_ENV !== "production";
|
|
110
208
|
const port = options.port ?? (Number(process.env.PORT) || 3303);
|
|
111
|
-
return { workspaceRoot, isDev, port };
|
|
209
|
+
return { workspaceRoot, workspaceRootInferred, isDev, port };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Peers stay lazy so requiring them is deferred to the factory that needs
|
|
213
|
+
* them — but a missing peer must name itself, not crash the config file. */
|
|
214
|
+
async function importPeer<T>(load: () => Promise<T>, specifier: string): Promise<T> {
|
|
215
|
+
try {
|
|
216
|
+
return await load();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[${PLUGIN_NAME}] could not import its peer dependency "${specifier}" — install it ` +
|
|
220
|
+
`next to ${PLUGIN_NAME} (it is a required peer): ${err instanceof Error ? err.message : err}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
112
223
|
}
|
|
113
224
|
|
|
114
225
|
function baseDefine(options: WebextTargetOptions, isDev: boolean): Record<string, unknown> {
|
|
@@ -121,10 +232,29 @@ function baseDefine(options: WebextTargetOptions, isDev: boolean): Record<string
|
|
|
121
232
|
};
|
|
122
233
|
}
|
|
123
234
|
|
|
124
|
-
|
|
235
|
+
const loggedNoPublicRoots = new Set<string>();
|
|
236
|
+
|
|
237
|
+
function baseResolve(
|
|
238
|
+
options: WebextTargetOptions,
|
|
239
|
+
workspaceRoot: string,
|
|
240
|
+
workspaceRootInferred: boolean,
|
|
241
|
+
) {
|
|
242
|
+
const sourceAliases = workspaceSourceAliases(workspaceRoot);
|
|
243
|
+
// Out-of-repo consumers have no public/ checkout, so the in-repo source
|
|
244
|
+
// aliasing silently no-ops — say so once, to make the divergence visible.
|
|
245
|
+
if (
|
|
246
|
+
workspaceRootInferred &&
|
|
247
|
+
Object.keys(sourceAliases).length === 0 &&
|
|
248
|
+
!loggedNoPublicRoots.has(workspaceRoot)
|
|
249
|
+
) {
|
|
250
|
+
loggedNoPublicRoots.add(workspaceRoot);
|
|
251
|
+
logger.info(
|
|
252
|
+
`[${PLUGIN_NAME}] no public/ under ${workspaceRoot} — resolving @multiplatform.one/* from node_modules`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
125
255
|
return {
|
|
126
256
|
alias: [
|
|
127
|
-
...Object.entries(
|
|
257
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
128
258
|
find,
|
|
129
259
|
replacement,
|
|
130
260
|
})),
|
|
@@ -160,8 +290,17 @@ export function listWebextViews(targetDir: string): string[] {
|
|
|
160
290
|
|
|
161
291
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
162
292
|
export async function createWebextViewsConfig(options: WebextTargetOptions): Promise<UserConfig> {
|
|
163
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
164
|
-
|
|
293
|
+
const { workspaceRoot, workspaceRootInferred, isDev, port } = resolveDefaults(options);
|
|
294
|
+
// Both vite-integration peers load lazily at factory-call time so a consumer
|
|
295
|
+
// following the peer contract sees a named error, not a module-scope crash.
|
|
296
|
+
const { default: react, reactCompilerPreset } = await importPeer(
|
|
297
|
+
() => import("@vitejs/plugin-react"),
|
|
298
|
+
"@vitejs/plugin-react",
|
|
299
|
+
);
|
|
300
|
+
const { tamaguiPlugin } = await importPeer(
|
|
301
|
+
() => import("@tamagui/vite-plugin"),
|
|
302
|
+
"@tamagui/vite-plugin",
|
|
303
|
+
);
|
|
165
304
|
const views = listWebextViews(options.targetDir);
|
|
166
305
|
return {
|
|
167
306
|
root: options.targetDir,
|
|
@@ -178,6 +317,7 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
178
317
|
config: options.tamaguiConfig,
|
|
179
318
|
outputCSS: path.join(options.targetDir, "tamagui.css"),
|
|
180
319
|
}) as Plugin,
|
|
320
|
+
tamaguiConfigBundleGuard(isDev),
|
|
181
321
|
],
|
|
182
322
|
define: baseDefine(options, isDev),
|
|
183
323
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -185,7 +325,7 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
185
325
|
jsx: "automatic",
|
|
186
326
|
target: "esnext",
|
|
187
327
|
},
|
|
188
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
328
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
189
329
|
server: {
|
|
190
330
|
port,
|
|
191
331
|
hmr: {
|
|
@@ -219,7 +359,7 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
219
359
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
220
360
|
* build — a single-file iife, no DOM. */
|
|
221
361
|
export function createWebextBackgroundConfig(options: WebextTargetOptions): UserConfig {
|
|
222
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
362
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
223
363
|
return {
|
|
224
364
|
root: options.targetDir,
|
|
225
365
|
plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
|
|
@@ -229,7 +369,7 @@ export function createWebextBackgroundConfig(options: WebextTargetOptions): User
|
|
|
229
369
|
jsx: "automatic",
|
|
230
370
|
target: "esnext",
|
|
231
371
|
},
|
|
232
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
372
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
233
373
|
build: {
|
|
234
374
|
watch: isDev ? {} : undefined,
|
|
235
375
|
outDir: path.join(options.stagingDir, "dist/background"),
|
|
@@ -254,8 +394,15 @@ export function createWebextBackgroundConfig(options: WebextTargetOptions): User
|
|
|
254
394
|
/** The content-script build — a single-file iife injected into pages, with
|
|
255
395
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
256
396
|
export async function createWebextContentConfig(options: WebextTargetOptions): Promise<UserConfig> {
|
|
257
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
258
|
-
const {
|
|
397
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
398
|
+
const { default: react, reactCompilerPreset } = await importPeer(
|
|
399
|
+
() => import("@vitejs/plugin-react"),
|
|
400
|
+
"@vitejs/plugin-react",
|
|
401
|
+
);
|
|
402
|
+
const { tamaguiPlugin } = await importPeer(
|
|
403
|
+
() => import("@tamagui/vite-plugin"),
|
|
404
|
+
"@tamagui/vite-plugin",
|
|
405
|
+
);
|
|
259
406
|
return {
|
|
260
407
|
root: options.targetDir,
|
|
261
408
|
plugins: [
|
|
@@ -271,6 +418,7 @@ export async function createWebextContentConfig(options: WebextTargetOptions): P
|
|
|
271
418
|
config: options.tamaguiConfig,
|
|
272
419
|
outputCSS: path.join(options.targetDir, "tamagui.content.css"),
|
|
273
420
|
}) as Plugin,
|
|
421
|
+
tamaguiConfigBundleGuard(isDev),
|
|
274
422
|
],
|
|
275
423
|
define: {
|
|
276
424
|
...baseDefine(options, isDev),
|
|
@@ -285,7 +433,7 @@ export async function createWebextContentConfig(options: WebextTargetOptions): P
|
|
|
285
433
|
jsx: "automatic",
|
|
286
434
|
target: "esnext",
|
|
287
435
|
},
|
|
288
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
436
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
289
437
|
build: {
|
|
290
438
|
watch: isDev ? {} : undefined,
|
|
291
439
|
outDir: path.join(options.stagingDir, "dist/contentScripts"),
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAMtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC;0CACsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB;2EACuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,WAAW,EAAE,iBAAiB,CAAC;IAC/B,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;qCACiC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;CACnB;AA4BD;;;iDAGiD;AACjD,wBAAgB,eAAe,IAAI,MAAM,CAYxC;AA2KD;2EAC2E;AAC3E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAO3D;AAED,qEAAqE;AACrE,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiE/F;AAED;0CAC0C;AAC1C,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CA+BrF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiEjG;AAID,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;uEAEmE;IACnE,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC5C;2DACuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;sCAEsC;AACtC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF;AAED;;mDAEmD;AACnD,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBtF;AAED,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED;uBACuB;AACvB,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBnF"}
|