@multiplatform.one/vite-plugin-webext 6.6.0 → 7.0.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 +109 -15
- package/lib/index.mjs +107 -13
- package/package.json +8 -4
- package/src/index.ts +168 -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,13 +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
|
-
let
|
|
35
|
-
|
|
35
|
+
let _rolldown_plugin_babel = require("@rolldown/plugin-babel");
|
|
36
|
+
_rolldown_plugin_babel = __toESM(_rolldown_plugin_babel, 1);
|
|
36
37
|
|
|
37
38
|
//#region src/index.ts
|
|
39
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
40
|
+
const logger = console;
|
|
38
41
|
const WEBEXT_EXTENSIONS = [
|
|
39
42
|
".webext.ts",
|
|
40
43
|
".webext.tsx",
|
|
@@ -76,13 +79,91 @@ function shikiStubPlugin() {
|
|
|
76
79
|
}
|
|
77
80
|
};
|
|
78
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
|
+
}
|
|
79
149
|
function resolveDefaults(options) {
|
|
150
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
80
151
|
return {
|
|
81
152
|
workspaceRoot: options.workspaceRoot ?? (0, _multiplatform_one_utils_dev.lookupProjectRoot)(),
|
|
153
|
+
workspaceRootInferred,
|
|
82
154
|
isDev: options.isDev ?? process.env.NODE_ENV !== "production",
|
|
83
155
|
port: options.port ?? (Number(process.env.PORT) || 3303)
|
|
84
156
|
};
|
|
85
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
|
+
}
|
|
86
167
|
function baseDefine(options, isDev) {
|
|
87
168
|
return {
|
|
88
169
|
__DEV__: isDev,
|
|
@@ -92,10 +173,16 @@ function baseDefine(options, isDev) {
|
|
|
92
173
|
...options.define
|
|
93
174
|
};
|
|
94
175
|
}
|
|
95
|
-
|
|
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
|
+
}
|
|
96
183
|
return {
|
|
97
184
|
alias: [
|
|
98
|
-
...Object.entries(
|
|
185
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
99
186
|
find,
|
|
100
187
|
replacement
|
|
101
188
|
})),
|
|
@@ -135,8 +222,9 @@ function listWebextViews(targetDir) {
|
|
|
135
222
|
}
|
|
136
223
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
137
224
|
async function createWebextViewsConfig(options) {
|
|
138
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
139
|
-
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");
|
|
140
228
|
const views = listWebextViews(options.targetDir);
|
|
141
229
|
return {
|
|
142
230
|
root: options.targetDir,
|
|
@@ -144,12 +232,14 @@ async function createWebextViewsConfig(options) {
|
|
|
144
232
|
plugins: [
|
|
145
233
|
(0, _multiplatform_one_utils_dev.stubExpoTypeDeclarations)(),
|
|
146
234
|
(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot),
|
|
147
|
-
(
|
|
235
|
+
react({ jsxRuntime: "automatic" }),
|
|
236
|
+
(0, _rolldown_plugin_babel.default)({ presets: [reactCompilerPreset()] }),
|
|
148
237
|
tamaguiPlugin({
|
|
149
238
|
components: (0, _multiplatform_one_utils_dev.lookupTamaguiModules)([options.targetDir]),
|
|
150
239
|
config: options.tamaguiConfig,
|
|
151
240
|
outputCSS: node_path.default.join(options.targetDir, "tamagui.css")
|
|
152
|
-
})
|
|
241
|
+
}),
|
|
242
|
+
tamaguiConfigBundleGuard(isDev)
|
|
153
243
|
],
|
|
154
244
|
define: baseDefine(options, isDev),
|
|
155
245
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -157,7 +247,7 @@ async function createWebextViewsConfig(options) {
|
|
|
157
247
|
jsx: "automatic",
|
|
158
248
|
target: "esnext"
|
|
159
249
|
},
|
|
160
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
250
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
161
251
|
server: {
|
|
162
252
|
port,
|
|
163
253
|
hmr: {
|
|
@@ -182,7 +272,7 @@ async function createWebextViewsConfig(options) {
|
|
|
182
272
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
183
273
|
* build — a single-file iife, no DOM. */
|
|
184
274
|
function createWebextBackgroundConfig(options) {
|
|
185
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
275
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
186
276
|
return {
|
|
187
277
|
root: options.targetDir,
|
|
188
278
|
plugins: [(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot)],
|
|
@@ -192,7 +282,7 @@ function createWebextBackgroundConfig(options) {
|
|
|
192
282
|
jsx: "automatic",
|
|
193
283
|
target: "esnext"
|
|
194
284
|
},
|
|
195
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
285
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
196
286
|
build: {
|
|
197
287
|
watch: isDev ? {} : void 0,
|
|
198
288
|
outDir: node_path.default.join(options.stagingDir, "dist/background"),
|
|
@@ -214,19 +304,23 @@ function createWebextBackgroundConfig(options) {
|
|
|
214
304
|
/** The content-script build — a single-file iife injected into pages, with
|
|
215
305
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
216
306
|
async function createWebextContentConfig(options) {
|
|
217
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
218
|
-
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");
|
|
219
310
|
return {
|
|
220
311
|
root: options.targetDir,
|
|
221
312
|
plugins: [
|
|
222
313
|
(0, _multiplatform_one_utils_dev.stubExpoTypeDeclarations)(),
|
|
223
314
|
(0, _multiplatform_one_utils_dev.oneServerOnlyBrowserStub)(workspaceRoot),
|
|
224
315
|
shikiStubPlugin(),
|
|
316
|
+
react({ jsxRuntime: "automatic" }),
|
|
317
|
+
(0, _rolldown_plugin_babel.default)({ presets: [reactCompilerPreset()] }),
|
|
225
318
|
tamaguiPlugin({
|
|
226
319
|
components: (0, _multiplatform_one_utils_dev.lookupTamaguiModules)([options.targetDir]),
|
|
227
320
|
config: options.tamaguiConfig,
|
|
228
321
|
outputCSS: node_path.default.join(options.targetDir, "tamagui.content.css")
|
|
229
|
-
})
|
|
322
|
+
}),
|
|
323
|
+
tamaguiConfigBundleGuard(isDev)
|
|
230
324
|
],
|
|
231
325
|
define: {
|
|
232
326
|
...baseDefine(options, isDev),
|
|
@@ -237,7 +331,7 @@ async function createWebextContentConfig(options) {
|
|
|
237
331
|
jsx: "automatic",
|
|
238
332
|
target: "esnext"
|
|
239
333
|
},
|
|
240
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
334
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
241
335
|
build: {
|
|
242
336
|
watch: isDev ? {} : void 0,
|
|
243
337
|
outDir: node_path.default.join(options.stagingDir, "dist/contentScripts"),
|
package/lib/index.mjs
CHANGED
|
@@ -1,9 +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
|
-
import
|
|
5
|
+
import babel from "@rolldown/plugin-babel";
|
|
5
6
|
|
|
6
7
|
//#region src/index.ts
|
|
8
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
9
|
+
const logger = console;
|
|
7
10
|
const WEBEXT_EXTENSIONS = [
|
|
8
11
|
".webext.ts",
|
|
9
12
|
".webext.tsx",
|
|
@@ -45,13 +48,91 @@ function shikiStubPlugin() {
|
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
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
|
+
}
|
|
48
118
|
function resolveDefaults(options) {
|
|
119
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
49
120
|
return {
|
|
50
121
|
workspaceRoot: options.workspaceRoot ?? lookupProjectRoot(),
|
|
122
|
+
workspaceRootInferred,
|
|
51
123
|
isDev: options.isDev ?? process.env.NODE_ENV !== "production",
|
|
52
124
|
port: options.port ?? (Number(process.env.PORT) || 3303)
|
|
53
125
|
};
|
|
54
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
|
+
}
|
|
55
136
|
function baseDefine(options, isDev) {
|
|
56
137
|
return {
|
|
57
138
|
__DEV__: isDev,
|
|
@@ -61,10 +142,16 @@ function baseDefine(options, isDev) {
|
|
|
61
142
|
...options.define
|
|
62
143
|
};
|
|
63
144
|
}
|
|
64
|
-
|
|
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
|
+
}
|
|
65
152
|
return {
|
|
66
153
|
alias: [
|
|
67
|
-
...Object.entries(
|
|
154
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
68
155
|
find,
|
|
69
156
|
replacement
|
|
70
157
|
})),
|
|
@@ -104,8 +191,9 @@ function listWebextViews(targetDir) {
|
|
|
104
191
|
}
|
|
105
192
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
106
193
|
async function createWebextViewsConfig(options) {
|
|
107
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
108
|
-
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");
|
|
109
197
|
const views = listWebextViews(options.targetDir);
|
|
110
198
|
return {
|
|
111
199
|
root: options.targetDir,
|
|
@@ -114,11 +202,13 @@ async function createWebextViewsConfig(options) {
|
|
|
114
202
|
stubExpoTypeDeclarations(),
|
|
115
203
|
oneServerOnlyBrowserStub(workspaceRoot),
|
|
116
204
|
react({ jsxRuntime: "automatic" }),
|
|
205
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
117
206
|
tamaguiPlugin({
|
|
118
207
|
components: lookupTamaguiModules([options.targetDir]),
|
|
119
208
|
config: options.tamaguiConfig,
|
|
120
209
|
outputCSS: path.join(options.targetDir, "tamagui.css")
|
|
121
|
-
})
|
|
210
|
+
}),
|
|
211
|
+
tamaguiConfigBundleGuard(isDev)
|
|
122
212
|
],
|
|
123
213
|
define: baseDefine(options, isDev),
|
|
124
214
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -126,7 +216,7 @@ async function createWebextViewsConfig(options) {
|
|
|
126
216
|
jsx: "automatic",
|
|
127
217
|
target: "esnext"
|
|
128
218
|
},
|
|
129
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
219
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
130
220
|
server: {
|
|
131
221
|
port,
|
|
132
222
|
hmr: {
|
|
@@ -151,7 +241,7 @@ async function createWebextViewsConfig(options) {
|
|
|
151
241
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
152
242
|
* build — a single-file iife, no DOM. */
|
|
153
243
|
function createWebextBackgroundConfig(options) {
|
|
154
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
244
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
155
245
|
return {
|
|
156
246
|
root: options.targetDir,
|
|
157
247
|
plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
|
|
@@ -161,7 +251,7 @@ function createWebextBackgroundConfig(options) {
|
|
|
161
251
|
jsx: "automatic",
|
|
162
252
|
target: "esnext"
|
|
163
253
|
},
|
|
164
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
254
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
165
255
|
build: {
|
|
166
256
|
watch: isDev ? {} : void 0,
|
|
167
257
|
outDir: path.join(options.stagingDir, "dist/background"),
|
|
@@ -183,19 +273,23 @@ function createWebextBackgroundConfig(options) {
|
|
|
183
273
|
/** The content-script build — a single-file iife injected into pages, with
|
|
184
274
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
185
275
|
async function createWebextContentConfig(options) {
|
|
186
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
187
|
-
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");
|
|
188
279
|
return {
|
|
189
280
|
root: options.targetDir,
|
|
190
281
|
plugins: [
|
|
191
282
|
stubExpoTypeDeclarations(),
|
|
192
283
|
oneServerOnlyBrowserStub(workspaceRoot),
|
|
193
284
|
shikiStubPlugin(),
|
|
285
|
+
react({ jsxRuntime: "automatic" }),
|
|
286
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
194
287
|
tamaguiPlugin({
|
|
195
288
|
components: lookupTamaguiModules([options.targetDir]),
|
|
196
289
|
config: options.tamaguiConfig,
|
|
197
290
|
outputCSS: path.join(options.targetDir, "tamagui.content.css")
|
|
198
|
-
})
|
|
291
|
+
}),
|
|
292
|
+
tamaguiConfigBundleGuard(isDev)
|
|
199
293
|
],
|
|
200
294
|
define: {
|
|
201
295
|
...baseDefine(options, isDev),
|
|
@@ -206,7 +300,7 @@ async function createWebextContentConfig(options) {
|
|
|
206
300
|
jsx: "automatic",
|
|
207
301
|
target: "esnext"
|
|
208
302
|
},
|
|
209
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
303
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
210
304
|
build: {
|
|
211
305
|
watch: isDev ? {} : void 0,
|
|
212
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.0.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",
|
|
@@ -46,11 +46,15 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@babel/core": "^7.26.0",
|
|
50
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
51
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
49
52
|
"chokidar": "^5.0.0",
|
|
50
|
-
"@multiplatform.one/utils": "
|
|
53
|
+
"@multiplatform.one/utils": "7.0.0"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"@tamagui/vite-plugin": "2.
|
|
56
|
+
"@tamagui/vite-plugin": "2.7.6",
|
|
57
|
+
"@types/babel__core": "^7.20.5",
|
|
54
58
|
"@types/node": "^25.6.0",
|
|
55
59
|
"@vitejs/plugin-react": "^6.0.1",
|
|
56
60
|
"typescript": "~5.9.3",
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
"vite": ">=5"
|
|
63
67
|
},
|
|
64
68
|
"scripts": {
|
|
65
|
-
"typecheck": "
|
|
69
|
+
"typecheck": "tsgo --noEmit",
|
|
66
70
|
"build": "rm -rf lib types 2>/dev/null && tsc -b --emitDeclarationOnly && tsdown"
|
|
67
71
|
}
|
|
68
72
|
}
|
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,
|
|
@@ -25,9 +26,13 @@ import {
|
|
|
25
26
|
workspaceSourceAliases,
|
|
26
27
|
lookupProjectRoot,
|
|
27
28
|
} from "@multiplatform.one/utils/dev";
|
|
28
|
-
import
|
|
29
|
+
import babel from "@rolldown/plugin-babel";
|
|
29
30
|
import type { Alias, Plugin, UserConfig } from "vite";
|
|
30
31
|
|
|
32
|
+
const PLUGIN_NAME = "@multiplatform.one/vite-plugin-webext";
|
|
33
|
+
|
|
34
|
+
const logger = console;
|
|
35
|
+
|
|
31
36
|
export interface WebextPackageInfo {
|
|
32
37
|
name: string;
|
|
33
38
|
displayName?: string;
|
|
@@ -103,11 +108,118 @@ export function shikiStubPlugin(): Plugin {
|
|
|
103
108
|
};
|
|
104
109
|
}
|
|
105
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
|
+
|
|
106
204
|
function resolveDefaults(options: WebextTargetOptions) {
|
|
205
|
+
const workspaceRootInferred = options.workspaceRoot == null;
|
|
107
206
|
const workspaceRoot = options.workspaceRoot ?? lookupProjectRoot();
|
|
108
207
|
const isDev = options.isDev ?? process.env.NODE_ENV !== "production";
|
|
109
208
|
const port = options.port ?? (Number(process.env.PORT) || 3303);
|
|
110
|
-
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
|
+
}
|
|
111
223
|
}
|
|
112
224
|
|
|
113
225
|
function baseDefine(options: WebextTargetOptions, isDev: boolean): Record<string, unknown> {
|
|
@@ -120,10 +232,29 @@ function baseDefine(options: WebextTargetOptions, isDev: boolean): Record<string
|
|
|
120
232
|
};
|
|
121
233
|
}
|
|
122
234
|
|
|
123
|
-
|
|
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
|
+
}
|
|
124
255
|
return {
|
|
125
256
|
alias: [
|
|
126
|
-
...Object.entries(
|
|
257
|
+
...Object.entries(sourceAliases).map(([find, replacement]) => ({
|
|
127
258
|
find,
|
|
128
259
|
replacement,
|
|
129
260
|
})),
|
|
@@ -159,8 +290,17 @@ export function listWebextViews(targetDir: string): string[] {
|
|
|
159
290
|
|
|
160
291
|
/** The extension pages build (popup/options/sidepanel/devtools…). */
|
|
161
292
|
export async function createWebextViewsConfig(options: WebextTargetOptions): Promise<UserConfig> {
|
|
162
|
-
const { workspaceRoot, isDev, port } = resolveDefaults(options);
|
|
163
|
-
|
|
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
|
+
);
|
|
164
304
|
const views = listWebextViews(options.targetDir);
|
|
165
305
|
return {
|
|
166
306
|
root: options.targetDir,
|
|
@@ -169,11 +309,15 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
169
309
|
stubExpoTypeDeclarations(),
|
|
170
310
|
oneServerOnlyBrowserStub(workspaceRoot),
|
|
171
311
|
react({ jsxRuntime: "automatic" }),
|
|
312
|
+
// Same React Compiler pass the One app runs (react 19 built-in
|
|
313
|
+
// runtime) — extension views get the memoization for free.
|
|
314
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
172
315
|
tamaguiPlugin({
|
|
173
316
|
components: lookupTamaguiModules([options.targetDir]),
|
|
174
317
|
config: options.tamaguiConfig,
|
|
175
318
|
outputCSS: path.join(options.targetDir, "tamagui.css"),
|
|
176
319
|
}) as Plugin,
|
|
320
|
+
tamaguiConfigBundleGuard(isDev),
|
|
177
321
|
],
|
|
178
322
|
define: baseDefine(options, isDev),
|
|
179
323
|
optimizeDeps: baseOptimizeDeps(),
|
|
@@ -181,7 +325,7 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
181
325
|
jsx: "automatic",
|
|
182
326
|
target: "esnext",
|
|
183
327
|
},
|
|
184
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
328
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
185
329
|
server: {
|
|
186
330
|
port,
|
|
187
331
|
hmr: {
|
|
@@ -215,7 +359,7 @@ export async function createWebextViewsConfig(options: WebextTargetOptions): Pro
|
|
|
215
359
|
/** The background service-worker (chromium) / background-script (firefox)
|
|
216
360
|
* build — a single-file iife, no DOM. */
|
|
217
361
|
export function createWebextBackgroundConfig(options: WebextTargetOptions): UserConfig {
|
|
218
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
362
|
+
const { workspaceRoot, workspaceRootInferred, isDev } = resolveDefaults(options);
|
|
219
363
|
return {
|
|
220
364
|
root: options.targetDir,
|
|
221
365
|
plugins: [oneServerOnlyBrowserStub(workspaceRoot)],
|
|
@@ -225,7 +369,7 @@ export function createWebextBackgroundConfig(options: WebextTargetOptions): User
|
|
|
225
369
|
jsx: "automatic",
|
|
226
370
|
target: "esnext",
|
|
227
371
|
},
|
|
228
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
372
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
229
373
|
build: {
|
|
230
374
|
watch: isDev ? {} : undefined,
|
|
231
375
|
outDir: path.join(options.stagingDir, "dist/background"),
|
|
@@ -250,19 +394,31 @@ export function createWebextBackgroundConfig(options: WebextTargetOptions): User
|
|
|
250
394
|
/** The content-script build — a single-file iife injected into pages, with
|
|
251
395
|
* Tamagui CSS extracted separately (webext.css web-accessible resource). */
|
|
252
396
|
export async function createWebextContentConfig(options: WebextTargetOptions): Promise<UserConfig> {
|
|
253
|
-
const { workspaceRoot, isDev } = resolveDefaults(options);
|
|
254
|
-
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
|
+
);
|
|
255
406
|
return {
|
|
256
407
|
root: options.targetDir,
|
|
257
408
|
plugins: [
|
|
258
409
|
stubExpoTypeDeclarations(),
|
|
259
410
|
oneServerOnlyBrowserStub(workspaceRoot),
|
|
260
411
|
shikiStubPlugin(),
|
|
412
|
+
// The react plugin transforms JSX (the esbuild jsx setting below
|
|
413
|
+
// stays for non-plugin paths); the babel pass adds the compiler.
|
|
414
|
+
react({ jsxRuntime: "automatic" }),
|
|
415
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
261
416
|
tamaguiPlugin({
|
|
262
417
|
components: lookupTamaguiModules([options.targetDir]),
|
|
263
418
|
config: options.tamaguiConfig,
|
|
264
419
|
outputCSS: path.join(options.targetDir, "tamagui.content.css"),
|
|
265
420
|
}) as Plugin,
|
|
421
|
+
tamaguiConfigBundleGuard(isDev),
|
|
266
422
|
],
|
|
267
423
|
define: {
|
|
268
424
|
...baseDefine(options, isDev),
|
|
@@ -277,7 +433,7 @@ export async function createWebextContentConfig(options: WebextTargetOptions): P
|
|
|
277
433
|
jsx: "automatic",
|
|
278
434
|
target: "esnext",
|
|
279
435
|
},
|
|
280
|
-
resolve: baseResolve(options, workspaceRoot),
|
|
436
|
+
resolve: baseResolve(options, workspaceRoot, workspaceRootInferred),
|
|
281
437
|
build: {
|
|
282
438
|
watch: isDev ? {} : undefined,
|
|
283
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":"
|
|
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"}
|