@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.
Files changed (100) hide show
  1. package/README.md +23 -1
  2. package/lib/bin/multiplatformOne.mjs +6 -4
  3. package/lib/commands/init.mjs +1 -1
  4. package/lib/commands/initApp.mjs +209 -8
  5. package/lib/commands/updateApp.mjs +170 -16
  6. package/package.json +2 -2
  7. package/scripts/clone.sh +1 -1
  8. package/scripts/update.sh +1 -1
  9. package/src/bin/multiplatformOne.ts +127 -108
  10. package/src/commands/init.ts +1 -1
  11. package/src/commands/initApp.spec.ts +369 -3
  12. package/src/commands/initApp.ts +325 -7
  13. package/src/commands/updateApp.spec.ts +81 -0
  14. package/src/commands/updateApp.ts +213 -12
  15. package/templates/app/package.json +19 -1
  16. package/templates/pieces/frappe/universal/README.md.partial +32 -0
  17. package/templates/pieces/frappe/universal/docker/compose.frappe.yaml +52 -0
  18. package/templates/pieces/frappe/universal/env.example.partial +8 -0
  19. package/templates/pieces/frappe/universal/packages/config/config.json.partial +3 -0
  20. package/templates/pieces/gnome/universal/README.md.partial +68 -0
  21. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/main.tsx +165 -0
  22. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/polyfills.ts +10 -0
  23. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/components.ts +36 -0
  24. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/empty-module.ts +28 -0
  25. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/forms.ts +18 -0
  26. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/one.ts +89 -0
  27. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/theme.ts +25 -0
  28. package/templates/pieces/gnome/universal/apps/__NAME__/gnome/tamagui-barrel.ts +77 -0
  29. package/templates/pieces/gnome/universal/apps/__NAME__/package.json.partial +18 -0
  30. package/templates/pieces/gnome/universal/apps/__NAME__/vite.config.gnome.ts +124 -0
  31. package/templates/pieces/gnome/universal/gitignore.partial +3 -0
  32. package/templates/pieces/gnome/universal/package.json.partial +6 -0
  33. package/templates/pieces/keycloak/universal/README.md.partial +19 -0
  34. package/templates/pieces/keycloak/universal/docker/compose.keycloak.yaml +27 -0
  35. package/templates/pieces/keycloak/universal/docker/keycloak/realm.json +43 -0
  36. package/templates/pieces/keycloak/universal/env.example.partial +9 -0
  37. package/templates/pieces/keycloak/universal/packages/config/config.json.partial +9 -0
  38. package/templates/pieces/tauri/universal/README.md.partial +18 -0
  39. package/templates/pieces/tauri/universal/apps/__NAME__/package.json.partial +10 -0
  40. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/Cargo.toml +26 -0
  41. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/build.rs +3 -0
  42. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/capabilities/default.json +7 -0
  43. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/128x128.png +0 -0
  44. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/128x128@2x.png +0 -0
  45. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/32x32.png +0 -0
  46. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/icon.icns +0 -0
  47. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/icons/icon.ico +0 -0
  48. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +14 -0
  49. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/main.rs +9 -0
  50. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +43 -0
  51. package/templates/pieces/tauri/universal/gitignore.partial +3 -0
  52. package/templates/pieces/tauri/universal/package.json.partial +6 -0
  53. package/templates/pieces/vscode/universal/README.md.partial +17 -0
  54. package/templates/pieces/vscode/universal/apps/__NAME__/package.json.partial +19 -0
  55. package/templates/pieces/vscode/universal/apps/__NAME__/vite.config.vscode.ts +17 -0
  56. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/env.d.ts +1 -0
  57. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/index.ts +13 -0
  58. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/views/helper.ts +14 -0
  59. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/extension/views/panel.ts +38 -0
  60. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/index.html +49 -0
  61. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/manifest.ts +33 -0
  62. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/package.json +7 -0
  63. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/tamagui.css +3 -0
  64. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/tsconfig.json +9 -0
  65. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/app.tsx +24 -0
  66. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/components/ErrorBoundary.tsx +41 -0
  67. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts/inter-400.woff2 +0 -0
  68. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts/inter-700.woff2 +0 -0
  69. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/fonts.css +21 -0
  70. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/main.tsx +35 -0
  71. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/router.tsx +58 -0
  72. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/useVSCodeTheme.ts +56 -0
  73. package/templates/pieces/vscode/universal/apps/__NAME__/vscode/webview/utils/vscode.ts +16 -0
  74. package/templates/pieces/vscode/universal/gitignore.partial +2 -0
  75. package/templates/pieces/vscode/universal/package.json.partial +6 -0
  76. package/templates/pieces/webext/universal/README.md.partial +16 -0
  77. package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +22 -0
  78. package/templates/pieces/webext/universal/apps/__NAME__/tsconfig.json.partial +5 -0
  79. package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext-background.ts +19 -0
  80. package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +35 -0
  81. package/templates/pieces/webext/universal/apps/__NAME__/webext/assets/fonts/inter-400.woff2 +0 -0
  82. package/templates/pieces/webext/universal/apps/__NAME__/webext/assets/fonts/inter-700.woff2 +0 -0
  83. package/templates/pieces/webext/universal/apps/__NAME__/webext/background/main.ts +10 -0
  84. package/templates/pieces/webext/universal/apps/__NAME__/webext/components/ErrorBoundary.tsx +41 -0
  85. package/templates/pieces/webext/universal/apps/__NAME__/webext/env.ts +2 -0
  86. package/templates/pieces/webext/universal/apps/__NAME__/webext/manifest.ts +45 -0
  87. package/templates/pieces/webext/universal/apps/__NAME__/webext/prepare.ts +21 -0
  88. package/templates/pieces/webext/universal/apps/__NAME__/webext/static/assets/icon-512.png +0 -0
  89. package/templates/pieces/webext/universal/apps/__NAME__/webext/tamagui.css +3 -0
  90. package/templates/pieces/webext/universal/apps/__NAME__/webext/tsconfig.json +9 -0
  91. package/templates/pieces/webext/universal/apps/__NAME__/webext/views/popup/index.html +77 -0
  92. package/templates/pieces/webext/universal/apps/__NAME__/webext/views/popup/main.tsx +73 -0
  93. package/templates/pieces/webext/universal/gitignore.partial +2 -0
  94. package/templates/pieces/webext/universal/package.json.partial +6 -0
  95. package/templates/universal/apps/__NAME__/package.json +1 -1
  96. package/templates/universal/package.json +25 -1
  97. package/types/commands/initApp.d.ts +43 -0
  98. package/types/commands/initApp.d.ts.map +1 -1
  99. package/types/commands/updateApp.d.ts +4 -0
  100. 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,3 @@
1
+ # tauri piece (mpo init --tauri): rust build output + generated schemas
2
+ apps/__NAME__/src-tauri/target
3
+ apps/__NAME__/src-tauri/gen/schemas
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "tauri:dev": "pnpm --filter @app/__NAME__ tauri:dev",
4
+ "tauri:build": "pnpm --filter @app/__NAME__ tauri:build"
5
+ }
6
+ }
@@ -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,3 @@
1
+ /* Generated by @tamagui/vite-plugin (outputCSS). This stub keeps the
2
+ `import "../tamagui.css"` in webview/main.tsx resolvable on a fresh
3
+ scaffold; dev/build regenerate it. */
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "types": ["node", "vscode", "vite/client"]
6
+ },
7
+ "include": ["**/*"],
8
+ "exclude": ["**/node_modules/**"]
9
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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,2 @@
1
+ # vscode piece (mpo init --vscode): staged extension host + webview
2
+ dist-vscode
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "vscode": "pnpm --filter @app/__NAME__ dev:vscode",
4
+ "build:vscode": "pnpm --filter @app/__NAME__ build:vscode"
5
+ }
6
+ }
@@ -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,5 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node"]
4
+ }
5
+ }
@@ -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
+ );