@multiplatform.one/cli 6.4.2 → 6.6.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 +23 -1
- package/lib/bin/multiplatformOne.mjs +6 -4
- package/lib/commands/init.mjs +1 -1
- package/lib/commands/initApp.mjs +209 -8
- package/lib/commands/updateApp.mjs +170 -16
- package/package.json +2 -2
- package/scripts/clone.sh +1 -1
- package/scripts/update.sh +1 -1
- package/src/bin/multiplatformOne.ts +127 -108
- package/src/commands/init.ts +1 -1
- package/src/commands/initApp.spec.ts +369 -3
- package/src/commands/initApp.ts +325 -7
- package/src/commands/updateApp.spec.ts +81 -0
- package/src/commands/updateApp.ts +213 -12
- package/templates/app/package.json +19 -1
- package/templates/pieces/frappe/universal/README.md.partial +32 -0
- package/templates/pieces/frappe/universal/docker/compose.frappe.yaml +52 -0
- package/templates/pieces/frappe/universal/env.example.partial +8 -0
- package/templates/pieces/frappe/universal/packages/config/config.json.partial +3 -0
- package/templates/pieces/gnome/universal/README.md.partial +68 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/main.tsx +165 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/polyfills.ts +10 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/components.ts +36 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/empty-module.ts +28 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/forms.ts +18 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/one.ts +89 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/theme.ts +25 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/tamagui-barrel.ts +77 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/package.json.partial +18 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/vite.config.gnome.ts +124 -0
- package/templates/pieces/gnome/universal/gitignore.partial +3 -0
- package/templates/pieces/gnome/universal/package.json.partial +6 -0
- package/templates/pieces/keycloak/universal/README.md.partial +19 -0
- package/templates/pieces/keycloak/universal/docker/compose.keycloak.yaml +27 -0
- package/templates/pieces/keycloak/universal/docker/keycloak/realm.json +43 -0
- package/templates/pieces/keycloak/universal/env.example.partial +9 -0
- package/templates/pieces/keycloak/universal/packages/config/config.json.partial +9 -0
- package/templates/pieces/tauri/universal/README.md.partial +18 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/package.json.partial +10 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/Cargo.toml +26 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/build.rs +3 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/capabilities/default.json +7 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/128x128.png +0 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/128x128@2x.png +0 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/32x32.png +0 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/icon.icns +0 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/icon.ico +0 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +14 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/main.rs +9 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +43 -0
- package/templates/pieces/tauri/universal/gitignore.partial +3 -0
- package/templates/pieces/tauri/universal/package.json.partial +6 -0
- package/templates/pieces/vscode/universal/README.md.partial +17 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/package.json.partial +19 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vite.config.vscode.ts +17 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/env.d.ts +1 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/index.ts +13 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/views/helper.ts +14 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/views/panel.ts +38 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/index.html +49 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/manifest.ts +33 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/package.json +7 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/tamagui.css +3 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/tsconfig.json +9 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/app.tsx +24 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/components/ErrorBoundary.tsx +41 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts/inter-400.woff2 +0 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts/inter-700.woff2 +0 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts.css +21 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/main.tsx +35 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/router.tsx +58 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/useVSCodeTheme.ts +56 -0
- package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/utils/vscode.ts +16 -0
- package/templates/pieces/vscode/universal/gitignore.partial +2 -0
- package/templates/pieces/vscode/universal/package.json.partial +6 -0
- package/templates/pieces/webext/universal/README.md.partial +16 -0
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +22 -0
- package/templates/pieces/webext/universal/apps/__NAME__/tsconfig.json.partial +5 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext-background.ts +19 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +35 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/assets/fonts/inter-400.woff2 +0 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/assets/fonts/inter-700.woff2 +0 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/background/main.ts +10 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/components/ErrorBoundary.tsx +41 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/env.ts +2 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/manifest.ts +45 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/prepare.ts +21 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/static/assets/icon-512.png +0 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/tamagui.css +3 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/tsconfig.json +9 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/views/popup/index.html +77 -0
- package/templates/pieces/webext/universal/apps/__NAME__/webext/views/popup/main.tsx +73 -0
- package/templates/pieces/webext/universal/gitignore.partial +2 -0
- package/templates/pieces/webext/universal/package.json.partial +6 -0
- package/templates/universal/apps/__NAME__/package.json +1 -1
- package/templates/universal/package.json +25 -1
- package/types/commands/initApp.d.ts +43 -0
- package/types/commands/initApp.d.ts.map +1 -1
- package/types/commands/updateApp.d.ts +4 -0
- package/types/commands/updateApp.d.ts.map +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//! __NAME__ Tauri desktop entry point — delegates to the shared lib.rs so
|
|
2
|
+
//! the same crate can later host mobile entry points (tauri ios/android).
|
|
3
|
+
|
|
4
|
+
// Prevents an additional console window on Windows in release builds.
|
|
5
|
+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
6
|
+
|
|
7
|
+
fn main() {
|
|
8
|
+
app_lib::run();
|
|
9
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schema.tauri.app/config/2",
|
|
3
|
+
"productName": "__NAME__",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"identifier": "com.__NAME_APPID__.app",
|
|
6
|
+
"build": {
|
|
7
|
+
"beforeDevCommand": "pnpm dev",
|
|
8
|
+
"beforeBuildCommand": "pnpm build",
|
|
9
|
+
"devUrl": "http://localhost:3456",
|
|
10
|
+
"frontendDist": "../dist"
|
|
11
|
+
},
|
|
12
|
+
"app": {
|
|
13
|
+
"security": {
|
|
14
|
+
"csp": null
|
|
15
|
+
},
|
|
16
|
+
"windows": [
|
|
17
|
+
{
|
|
18
|
+
"title": "__NAME_PASCAL__",
|
|
19
|
+
"width": 1200,
|
|
20
|
+
"height": 800,
|
|
21
|
+
"resizable": true,
|
|
22
|
+
"fullscreen": false,
|
|
23
|
+
"center": true,
|
|
24
|
+
"minWidth": 800,
|
|
25
|
+
"minHeight": 600
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"bundle": {
|
|
30
|
+
"active": true,
|
|
31
|
+
"targets": "all",
|
|
32
|
+
"icon": [
|
|
33
|
+
"icons/32x32.png",
|
|
34
|
+
"icons/128x128.png",
|
|
35
|
+
"icons/128x128@2x.png",
|
|
36
|
+
"icons/icon.icns",
|
|
37
|
+
"icons/icon.ico"
|
|
38
|
+
],
|
|
39
|
+
"category": "Productivity",
|
|
40
|
+
"shortDescription": "__NAME__ desktop app",
|
|
41
|
+
"copyright": "Copyright © __YEAR__"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## VS Code extension (`mpo init --vscode`)
|
|
2
|
+
|
|
3
|
+
`apps/__NAME__/vscode/` is a VS Code extension target: a thin extension host
|
|
4
|
+
(`extension/`) opening a Tamagui webview (`webview/`) that mounts the same
|
|
5
|
+
`features/__NAME__` screens as the web app — a webview has no One server
|
|
6
|
+
runtime, so the `one` router API rides the stack-navigation seam
|
|
7
|
+
(`@multiplatform.one/router/seam`).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm build:vscode # stage extension + webview + manifest into apps/__NAME__/dist-vscode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Try it: open `apps/__NAME__/dist-vscode` in VS Code's Extension Development
|
|
14
|
+
Host (F5 with a launch config pointing at it, or
|
|
15
|
+
`code --extensionDevelopmentPath=$PWD/apps/__NAME__/dist-vscode`), then run
|
|
16
|
+
the "__NAME_PASCAL__: Show App" command. Package a .vsix with
|
|
17
|
+
`vsce package` from inside `apps/__NAME__/dist-vscode`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"dev:vscode": "cd vscode && vite --config ../vite.config.vscode.ts",
|
|
4
|
+
"build:vscode": "cd vscode && vite build --config ../vite.config.vscode.ts && cd .. && tsx vscode/manifest.ts",
|
|
5
|
+
"typecheck:vscode": "tsc --noEmit -p vscode/tsconfig.json"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@multiplatform.one/router": "__MPO_VERSION__",
|
|
9
|
+
"react-cookie": "^8.1.2"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@multiplatform.one/vite-plugin-vscode": "__MPO_VERSION__",
|
|
13
|
+
"@tamagui/vite-plugin": "2.0.0-rc.41",
|
|
14
|
+
"@tomjs/vite-plugin-vscode": "^7.1.1",
|
|
15
|
+
"@types/node": "^25.6.0",
|
|
16
|
+
"@types/vscode": "^1.118.0",
|
|
17
|
+
"tsx": "^4.21.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Vite config for the VS Code target — extension host + Tamagui webview
|
|
2
|
+
// built from vscode/ into dist-vscode/, where vscode/manifest.ts drops the
|
|
3
|
+
// vsce package.json. The heavy lifting (workspace source aliases, one→seam
|
|
4
|
+
// alias, React dedupe, @tomjs/vite-plugin-vscode wiring) lives in
|
|
5
|
+
// @multiplatform.one/vite-plugin-vscode.
|
|
6
|
+
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { createVscodeConfig } from "@multiplatform.one/vite-plugin-vscode";
|
|
9
|
+
import { defineConfig } from "vite";
|
|
10
|
+
|
|
11
|
+
export default defineConfig(async () =>
|
|
12
|
+
createVscodeConfig({
|
|
13
|
+
targetDir: resolve(import.meta.dirname, "vscode"),
|
|
14
|
+
outDir: resolve(import.meta.dirname, "dist-vscode"),
|
|
15
|
+
tamaguiConfig: resolve(import.meta.dirname, "config/tamagui.config.ts"),
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@tomjs/vite-plugin-vscode/env" />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExtensionContext } from "vscode";
|
|
2
|
+
import { commands } from "vscode";
|
|
3
|
+
import { MainPanel } from "./views/panel";
|
|
4
|
+
|
|
5
|
+
export function activate(context: ExtensionContext) {
|
|
6
|
+
context.subscriptions.push(
|
|
7
|
+
commands.registerCommand("__NAME__.showApp", async () => {
|
|
8
|
+
MainPanel.render(context);
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deactivate() {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtensionContext, Webview } from "vscode";
|
|
2
|
+
// The plugin resolves `virtual:vscode` at build time to a module embedding
|
|
3
|
+
// the built webview HTML (CSP + nonce + asWebviewUri asset rewriting).
|
|
4
|
+
import { getWebviewHtml } from "virtual:vscode";
|
|
5
|
+
|
|
6
|
+
export class WebviewHelper {
|
|
7
|
+
public static setupHtml(webview: Webview, context: ExtensionContext) {
|
|
8
|
+
return getWebviewHtml(
|
|
9
|
+
process.env.VITE_DEV_SERVER_URL
|
|
10
|
+
? { serverUrl: process.env.VITE_DEV_SERVER_URL, webview, context }
|
|
11
|
+
: { webview, context },
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Disposable, ExtensionContext, WebviewPanel } from "vscode";
|
|
2
|
+
import { ViewColumn, window } from "vscode";
|
|
3
|
+
import { WebviewHelper } from "./helper";
|
|
4
|
+
|
|
5
|
+
export class MainPanel {
|
|
6
|
+
public static currentPanel: MainPanel | undefined;
|
|
7
|
+
private readonly _panel: WebviewPanel;
|
|
8
|
+
private _disposables: Disposable[] = [];
|
|
9
|
+
|
|
10
|
+
private constructor(panel: WebviewPanel, context: ExtensionContext) {
|
|
11
|
+
this._panel = panel;
|
|
12
|
+
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
|
13
|
+
this._panel.webview.html = WebviewHelper.setupHtml(this._panel.webview, context);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static render(context: ExtensionContext) {
|
|
17
|
+
if (MainPanel.currentPanel) {
|
|
18
|
+
MainPanel.currentPanel._panel.reveal(ViewColumn.One);
|
|
19
|
+
} else {
|
|
20
|
+
const panel = window.createWebviewPanel("__NAME__.app", "__NAME_PASCAL__", ViewColumn.One, {
|
|
21
|
+
enableScripts: true,
|
|
22
|
+
});
|
|
23
|
+
MainPanel.currentPanel = new MainPanel(panel, context);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Clean up webview resources when the panel is closed. */
|
|
28
|
+
public dispose() {
|
|
29
|
+
MainPanel.currentPanel = undefined;
|
|
30
|
+
this._panel.dispose();
|
|
31
|
+
while (this._disposables.length) {
|
|
32
|
+
const disposable = this._disposables.pop();
|
|
33
|
+
if (disposable) {
|
|
34
|
+
disposable.dispose();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__NAME_PASCAL__</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Critical reset — matches the web app shell's inline reset */
|
|
9
|
+
html {
|
|
10
|
+
max-width: 100vw;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
overflow-x: hidden;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
min-width: 100%;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
#root {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
min-width: 100%;
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
}
|
|
27
|
+
* {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
a {
|
|
31
|
+
color: inherit;
|
|
32
|
+
text-decoration: none;
|
|
33
|
+
}
|
|
34
|
+
h1,
|
|
35
|
+
h2,
|
|
36
|
+
h3,
|
|
37
|
+
h4,
|
|
38
|
+
h5,
|
|
39
|
+
h6 {
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: 0;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="root"></div>
|
|
47
|
+
<script type="module" src="/webview/main.tsx"></script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Stage the VS Code extension manifest after `pnpm build:vscode`: a VS Code
|
|
2
|
+
// extension's package.json IS its manifest, and the app's own package.json
|
|
3
|
+
// is a One/Expo app manifest — so `vsce package` runs against dist-vscode/
|
|
4
|
+
// with this generated one (same pattern as the webext target's generated
|
|
5
|
+
// manifest.json).
|
|
6
|
+
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { writeVsceManifest } from "@multiplatform.one/vite-plugin-vscode";
|
|
9
|
+
|
|
10
|
+
const stagingDir = resolve(import.meta.dirname, "../dist-vscode");
|
|
11
|
+
|
|
12
|
+
await writeVsceManifest(stagingDir, {
|
|
13
|
+
name: "__NAME__",
|
|
14
|
+
displayName: "__NAME_PASCAL__",
|
|
15
|
+
publisher: "__NAME__",
|
|
16
|
+
version: "0.0.0",
|
|
17
|
+
description: "__NAME__ VS Code extension",
|
|
18
|
+
// The extension host builds ESM (the app is type:module) — supported
|
|
19
|
+
// since VS Code 1.100.
|
|
20
|
+
type: "module",
|
|
21
|
+
engines: { vscode: "^1.100.0" },
|
|
22
|
+
activationEvents: [],
|
|
23
|
+
contributes: {
|
|
24
|
+
commands: [
|
|
25
|
+
{
|
|
26
|
+
command: "__NAME__.showApp",
|
|
27
|
+
title: "__NAME_PASCAL__: Show App",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log(`[vscode] staged extension manifest → ${stagingDir}/package.json`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/__NAME__-vscode-target",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "Build-time marker for the VS Code target: @tomjs/vite-plugin-vscode reads the nearest package.json (cwd) for `main` (presence check) and `type` (esm/cjs extension-host format). NOT a workspace package (apps/* only matches apps/__NAME__) and NOT the extension manifest — that is generated into dist-vscode by vscode/manifest.ts. The app's own package.json cannot carry `main`: Expo/Metro would treat it as the native app entry.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/extension/index.js"
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CookiesProvider } from "react-cookie";
|
|
2
|
+
import { AppProvider } from "../../src/app";
|
|
3
|
+
import { ErrorBoundary } from "./components/ErrorBoundary";
|
|
4
|
+
import { AppRoutes, initialRoute } from "./router";
|
|
5
|
+
import { useVSCodeTheme } from "./useVSCodeTheme";
|
|
6
|
+
|
|
7
|
+
export function App() {
|
|
8
|
+
// VS Code's theme kind drives the house ThemeProvider scheme, so the
|
|
9
|
+
// webview follows the editor's light/dark theme live.
|
|
10
|
+
const scheme = useVSCodeTheme();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ErrorBoundary>
|
|
14
|
+
{/* Webview pages don't go through createRootLayout, which normally
|
|
15
|
+
provides cookies (ThemeProvider's useCookies throws without it) —
|
|
16
|
+
same fix as the webext extension pages. */}
|
|
17
|
+
<CookiesProvider>
|
|
18
|
+
<AppProvider systemTheme={scheme}>
|
|
19
|
+
<AppRoutes initialHref={initialRoute()} />
|
|
20
|
+
</AppProvider>
|
|
21
|
+
</CookiesProvider>
|
|
22
|
+
</ErrorBoundary>
|
|
23
|
+
);
|
|
24
|
+
}
|
package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/components/ErrorBoundary.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plain-DOM error boundary (same pattern as the webext target): it must
|
|
5
|
+
* render without Tamagui so a failure inside the provider stack itself still
|
|
6
|
+
* surfaces a readable error instead of a blank webview.
|
|
7
|
+
*/
|
|
8
|
+
export class ErrorBoundary extends Component<
|
|
9
|
+
{ children: ReactNode },
|
|
10
|
+
{ hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null }
|
|
11
|
+
> {
|
|
12
|
+
constructor(props: { children: ReactNode }) {
|
|
13
|
+
super(props);
|
|
14
|
+
this.state = { hasError: false, error: null, errorInfo: null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getDerivedStateFromError(error: Error) {
|
|
18
|
+
return { hasError: true, error };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
22
|
+
this.setState({ error, errorInfo });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
if (this.state.hasError) {
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ padding: 16, color: "red" }}>
|
|
29
|
+
<h2>Something went wrong</h2>
|
|
30
|
+
<details style={{ whiteSpace: "pre-wrap", marginTop: 16 }}>
|
|
31
|
+
<summary>Error Details</summary>
|
|
32
|
+
{this.state.error?.toString()}
|
|
33
|
+
<br />
|
|
34
|
+
{this.state.errorInfo?.componentStack}
|
|
35
|
+
</details>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return this.props.children;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Inter must be declared in CSS that ships as a real asset: the webview
|
|
3
|
+
* rewrites <link href> to vscode-resource URIs, and url() inside the emitted
|
|
4
|
+
* stylesheet resolves relative to that stylesheet — inline <style> in the
|
|
5
|
+
* webview HTML would resolve against the vscode-webview:// document instead.
|
|
6
|
+
* Same woff2 binaries as @tamagui/font-inter (identical metrics).
|
|
7
|
+
*/
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: "Inter";
|
|
10
|
+
font-style: normal;
|
|
11
|
+
font-weight: 400;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
src: url("./fonts/inter-400.woff2") format("woff2");
|
|
14
|
+
}
|
|
15
|
+
@font-face {
|
|
16
|
+
font-family: "Inter";
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
font-display: swap;
|
|
20
|
+
src: url("./fonts/inter-700.woff2") format("woff2");
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "./fonts.css";
|
|
2
|
+
import "../tamagui.css";
|
|
3
|
+
import "@tamagui/core/reset.css";
|
|
4
|
+
import {
|
|
5
|
+
createI18nConfig,
|
|
6
|
+
defaultLanguage,
|
|
7
|
+
defaultNamespace,
|
|
8
|
+
languages,
|
|
9
|
+
namespaces,
|
|
10
|
+
} from "@package/i18n";
|
|
11
|
+
import { resources } from "@package/i18n/resources";
|
|
12
|
+
import i18next from "i18next";
|
|
13
|
+
import { StrictMode } from "react";
|
|
14
|
+
import { createRoot } from "react-dom/client";
|
|
15
|
+
import { initReactI18next } from "react-i18next";
|
|
16
|
+
import { App } from "./app";
|
|
17
|
+
|
|
18
|
+
// Initialize i18n synchronously before React renders: the webview has no One
|
|
19
|
+
// server runtime, so translations come purely from the bundled resources.
|
|
20
|
+
i18next.use(initReactI18next).init(
|
|
21
|
+
createI18nConfig({
|
|
22
|
+
languages: [...languages],
|
|
23
|
+
namespaces: [...namespaces],
|
|
24
|
+
defaultLanguage,
|
|
25
|
+
defaultNamespace,
|
|
26
|
+
resources,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const root = createRoot(document.getElementById("root")!);
|
|
31
|
+
root.render(
|
|
32
|
+
<StrictMode>
|
|
33
|
+
<App />
|
|
34
|
+
</StrictMode>,
|
|
35
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The webview runs the shared feature screens over the stack-navigation
|
|
2
|
+
// seam (@multiplatform.one/router/seam): a webview has no address bar and
|
|
3
|
+
// no One server runtime, so One's file-based routes/loaders don't apply —
|
|
4
|
+
// the seam backs the SAME `one` router API (aliased in
|
|
5
|
+
// vite.config.vscode.ts) with a DOM page stack. AppLayout and the screens
|
|
6
|
+
// are the exact modules the web/native app mounts.
|
|
7
|
+
|
|
8
|
+
import { AboutScreen, AppLayout, HomeScreen } from "@features/__NAME__";
|
|
9
|
+
import { DomNavigationHost, stackNavigator } from "@multiplatform.one/router/seam";
|
|
10
|
+
import { usePathname } from "one";
|
|
11
|
+
import { useEffect } from "react";
|
|
12
|
+
import { vscode } from "./utils/vscode";
|
|
13
|
+
|
|
14
|
+
export const ROUTES = [
|
|
15
|
+
{ pattern: "/", tag: "home", title: "Home" },
|
|
16
|
+
{ pattern: "/about", tag: "about", title: "About" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
stackNavigator.configure(ROUTES);
|
|
20
|
+
|
|
21
|
+
/** Restore the route persisted through the webview host (reopening the
|
|
22
|
+
* panel / a VS Code reload). */
|
|
23
|
+
export function initialRoute(): string {
|
|
24
|
+
const state = vscode.getState();
|
|
25
|
+
if (
|
|
26
|
+
state &&
|
|
27
|
+
typeof state === "object" &&
|
|
28
|
+
typeof (state as { route?: unknown }).route === "string"
|
|
29
|
+
) {
|
|
30
|
+
return (state as { route: string }).route;
|
|
31
|
+
}
|
|
32
|
+
return "/";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function RoutePersistence() {
|
|
36
|
+
const pathname = usePathname();
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
vscode.setState({ route: pathname });
|
|
40
|
+
}, [pathname]);
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function AppRoutes({ initialHref }: { initialHref?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<AppLayout>
|
|
48
|
+
<RoutePersistence />
|
|
49
|
+
<DomNavigationHost
|
|
50
|
+
initialHref={initialHref}
|
|
51
|
+
routes={[
|
|
52
|
+
{ ...ROUTES[0]!, element: <HomeScreen /> },
|
|
53
|
+
{ ...ROUTES[1]!, element: <AboutScreen /> },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
</AppLayout>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type VSCodeScheme = "light" | "dark";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the house theme scheme from the VS Code webview host.
|
|
7
|
+
*
|
|
8
|
+
* VS Code stamps the webview <body> with a theme class
|
|
9
|
+
* (`vscode-light` | `vscode-dark` | `vscode-high-contrast`) and a
|
|
10
|
+
* `data-vscode-theme-kind` attribute. Outside VS Code (browser harness, dev
|
|
11
|
+
* preview) neither exists, so fall back to `prefers-color-scheme`.
|
|
12
|
+
*/
|
|
13
|
+
function readScheme(): VSCodeScheme {
|
|
14
|
+
if (typeof document === "undefined") return "light";
|
|
15
|
+
const body = document.body;
|
|
16
|
+
const kind = body?.dataset?.vscodeThemeKind;
|
|
17
|
+
if (kind) return kind.includes("light") ? "light" : "dark";
|
|
18
|
+
if (body?.classList.contains("vscode-high-contrast-light")) return "light";
|
|
19
|
+
if (body?.classList.contains("vscode-light")) return "light";
|
|
20
|
+
if (body?.classList.contains("vscode-dark") || body?.classList.contains("vscode-high-contrast")) {
|
|
21
|
+
return "dark";
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
typeof window !== "undefined" &&
|
|
25
|
+
window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
|
26
|
+
) {
|
|
27
|
+
return "dark";
|
|
28
|
+
}
|
|
29
|
+
return "light";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Live VS Code theme scheme: VS Code mutates the body class/data attributes
|
|
34
|
+
* in place when the user switches color themes, so a MutationObserver keeps
|
|
35
|
+
* the scheme in sync without extension-host message plumbing.
|
|
36
|
+
*/
|
|
37
|
+
export function useVSCodeTheme(): VSCodeScheme {
|
|
38
|
+
const [scheme, setScheme] = useState<VSCodeScheme>(readScheme);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const update = () => setScheme(readScheme());
|
|
42
|
+
const observer = new MutationObserver(update);
|
|
43
|
+
observer.observe(document.body, {
|
|
44
|
+
attributes: true,
|
|
45
|
+
attributeFilter: ["class", "data-vscode-theme-kind"],
|
|
46
|
+
});
|
|
47
|
+
const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
48
|
+
mq?.addEventListener("change", update);
|
|
49
|
+
return () => {
|
|
50
|
+
observer.disconnect();
|
|
51
|
+
mq?.removeEventListener("change", update);
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return scheme;
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `acquireVsCodeApi` is injected by the VS Code webview host. When the
|
|
2
|
+
// webview is opened directly in a browser (dev preview), it doesn't exist —
|
|
3
|
+
// fall back to a noop stub so the page can still render.
|
|
4
|
+
declare function acquireVsCodeApi(): {
|
|
5
|
+
postMessage: (msg: unknown) => void;
|
|
6
|
+
setState: (state: unknown) => void;
|
|
7
|
+
getState: () => unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const stub = {
|
|
11
|
+
postMessage: () => {},
|
|
12
|
+
setState: () => {},
|
|
13
|
+
getState: () => undefined,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const vscode = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : stub;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## Browser extension (`mpo init --webext`)
|
|
2
|
+
|
|
3
|
+
`apps/__NAME__/webext/` is an MV3 extension target (popup + background) that
|
|
4
|
+
mounts the same `features/__NAME__` screens as the web app — extension pages
|
|
5
|
+
have no One server runtime, so the `one` router API rides the
|
|
6
|
+
stack-navigation seam (`@multiplatform.one/router/seam`).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm build:webext # stage a loadable extension into apps/__NAME__/dist-webext
|
|
10
|
+
pnpm webext # dev loop: manifest watcher + background watch + views HMR
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Load `apps/__NAME__/dist-webext` as an unpacked extension
|
|
14
|
+
(chrome://extensions → Developer mode → Load unpacked; Firefox loads it via
|
|
15
|
+
about:debugging). The manifest lives in code at
|
|
16
|
+
`apps/__NAME__/webext/manifest.ts`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"dev:webext": "NODE_ENV=development sh -c 'tsx webext/prepare.ts & vite build --config vite.config.webext-background.ts --mode development & vite --config vite.config.webext.ts & wait'",
|
|
4
|
+
"build:webext": "vite build --config vite.config.webext.ts && vite build --config vite.config.webext-background.ts && NODE_ENV=production tsx webext/prepare.ts",
|
|
5
|
+
"typecheck:webext": "tsc --noEmit -p webext/tsconfig.json"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@multiplatform.one/router": "__MPO_VERSION__",
|
|
9
|
+
"react-cookie": "^8.1.2",
|
|
10
|
+
"webextension-polyfill": "^0.12.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@multiplatform.one/utils": "__MPO_VERSION__",
|
|
14
|
+
"@multiplatform.one/vite-plugin-webext": "__MPO_VERSION__",
|
|
15
|
+
"@tamagui/vite-plugin": "2.0.0-rc.41",
|
|
16
|
+
"@tamagui/web": "2.0.0-rc.41",
|
|
17
|
+
"@types/chrome": "^0.2.5",
|
|
18
|
+
"@types/node": "^25.6.0",
|
|
19
|
+
"dotenv": "^17.4.2",
|
|
20
|
+
"tsx": "^4.21.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Vite config for the webext background target (MV3 service worker on
|
|
2
|
+
// chromium, background script on firefox) — see vite.config.webext.ts.
|
|
3
|
+
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { createWebextBackgroundConfig } from "@multiplatform.one/vite-plugin-webext";
|
|
6
|
+
import { defineConfig } from "vite";
|
|
7
|
+
import packageJson from "./package.json";
|
|
8
|
+
import { isDev, port } from "./webext/env";
|
|
9
|
+
|
|
10
|
+
export default defineConfig(
|
|
11
|
+
createWebextBackgroundConfig({
|
|
12
|
+
targetDir: resolve(import.meta.dirname, "webext"),
|
|
13
|
+
stagingDir: resolve(import.meta.dirname, "dist-webext"),
|
|
14
|
+
tamaguiConfig: resolve(import.meta.dirname, "config/tamagui.config.ts"),
|
|
15
|
+
packageInfo: packageJson,
|
|
16
|
+
isDev,
|
|
17
|
+
port,
|
|
18
|
+
}),
|
|
19
|
+
);
|