code2app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/assets/icon-source.png +0 -0
- package/bin/cli.js +248 -0
- package/dist/index.html +24 -0
- package/docs/APP_STORES.md +129 -0
- package/docs/BUILDING.md +134 -0
- package/docs/LOCAL_APPS.md +116 -0
- package/docs/MOBILE.md +78 -0
- package/docs/OAUTH.md +76 -0
- package/package.json +66 -0
- package/profiles/README.md +56 -0
- package/profiles/example.json +37 -0
- package/scripts/build-sidecar.mjs +80 -0
- package/scripts/configure.mjs +222 -0
- package/scripts/generate-icons.mjs +53 -0
- package/scripts/generate-placeholder-icons.mjs +186 -0
- package/scripts/lib/png.mjs +141 -0
- package/scripts/profile.mjs +147 -0
- package/src-tauri/Cargo.lock +5832 -0
- package/src-tauri/Cargo.toml +47 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +15 -0
- package/src-tauri/capabilities/remote.json +17 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/64x64.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/generated_config.rs +22 -0
- package/src-tauri/src/lib.rs +212 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/tauri.conf.json +84 -0
- package/vitest.config.mjs +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!-- template-git-repo:badges:start -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://starterdocs.vtempest.workers.dev/docs/packages/native-app-wrapper"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
|
|
4
|
+
<br />
|
|
5
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Stars" /></a>
|
|
6
|
+
<br />
|
|
7
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/issues"><img src="https://img.shields.io/github/issues/OpenSourceAGI/dev-tools-starter-agent?logo=github" alt="GitHub Issues" /></a>
|
|
8
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls"><img src="https://img.shields.io/github/issues-pr/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs" alt="Open Pull Requests" /></a>
|
|
9
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls?q=is%3Apr+is%3Aclosed"><img src="https://img.shields.io/github/issues-pr-closed/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs%20merged&color=8957e5" alt="Merged Pull Requests" /></a>
|
|
10
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/discussions"><img src="https://img.shields.io/github/discussions/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Discussions" /></a>
|
|
11
|
+
<a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/commits/master/"><img src="https://img.shields.io/github/last-commit/OpenSourceAGI/dev-tools-starter-agent.svg" alt="GitHub last commit" /></a>
|
|
12
|
+
<br />
|
|
13
|
+
<img src="https://img.shields.io/badge/Bun-14151A?logo=bun&logoColor=white" alt="Bun" /> <img src="https://img.shields.io/badge/Tauri-24C8D8?logo=tauri&logoColor=white" alt="Tauri" /> <img src="https://img.shields.io/badge/Vitest-6E9F18?logo=vitest&logoColor=white" alt="Vitest" />
|
|
14
|
+
</p>
|
|
15
|
+
<!-- template-git-repo:badges:end -->
|
|
16
|
+
|
|
17
|
+
<!-- skills:install:start -->
|
|
18
|
+
**🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill native-app-wrapper` ([what it covers](../../skills/native-app-wrapper/SKILL.md))
|
|
19
|
+
<!-- skills:install:end -->
|
|
20
|
+
|
|
21
|
+
# native-app-wrapper
|
|
22
|
+
|
|
23
|
+
A [Tauri](https://tauri.app) shell that turns one JSON profile into a native desktop
|
|
24
|
+
(Windows/macOS/Linux) and mobile (Android/iOS) app. It packages one of two things:
|
|
25
|
+
|
|
26
|
+
- **A website** (`"mode": "remote"`) — a titled window that loads the site directly, plus the
|
|
27
|
+
OS-level plumbing a plain webview doesn't get for free: a real app icon on every platform, a
|
|
28
|
+
Google-OAuth-compatible login handoff, and a fullscreen toggle.
|
|
29
|
+
- **A command-line tool** (`"mode": "local"`) — a bundled HTML frontend backed by the CLI itself,
|
|
30
|
+
shipped alongside the app binary as a [Tauri sidecar](https://v2.tauri.app/develop/sidecar/), so
|
|
31
|
+
the app runs with no Node, Python, or other runtime installed on the user's machine. See
|
|
32
|
+
[`docs/LOCAL_APPS.md`](docs/LOCAL_APPS.md).
|
|
33
|
+
|
|
34
|
+
`packages/about-system-info/native` is a working example of the second kind — the `about-system`
|
|
35
|
+
CLI shipped as an installable desktop app for all three desktop OSes.
|
|
36
|
+
|
|
37
|
+
## What this is not
|
|
38
|
+
|
|
39
|
+
Not a place for app-specific UI or logic. In remote mode the wrapped site provides all of that; in
|
|
40
|
+
local mode the app's own `dist/` and its sidecar do. This package's job stops at: open a window,
|
|
41
|
+
register the app's identity (icon, name, bundle id) with each OS, hand a browser-based OAuth login
|
|
42
|
+
back to that window, and expose the bundled CLI's output to the bundled frontend. Anything more
|
|
43
|
+
belongs in the app being wrapped.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
Scaffold a copy of the wrapper for your own app:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd packages/native-app-wrapper
|
|
51
|
+
node bin/cli.js init ../../apps/my-app/native --profile-file ./my-app.json
|
|
52
|
+
cd ../../apps/my-app/native
|
|
53
|
+
npm install
|
|
54
|
+
npm run dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`init` copies the wrapper's sources into the target directory, gives that copy your profile as its
|
|
58
|
+
only identity, regenerates the Tauri config and Rust constants from it, renames the crate, and
|
|
59
|
+
draws a placeholder icon set so the copy builds immediately. It also leaves out what the profile
|
|
60
|
+
doesn't use: a desktop-only profile gets no `android:build` script, an offline CLI app gets no
|
|
61
|
+
guide to browser OAuth handoffs. Nothing links back to this package afterward — the copy is a
|
|
62
|
+
standalone app you can commit next to the thing it wraps, and updating it later means copying the
|
|
63
|
+
changed scripts across, not re-running `init` over your own `dist/`.
|
|
64
|
+
|
|
65
|
+
To work on this package itself instead, `npm run dev` here opens a window on the untouched
|
|
66
|
+
`profiles/example.json` (`https://example.com`), which is what the checked-in
|
|
67
|
+
`src-tauri/tauri.conf.json` is generated from.
|
|
68
|
+
|
|
69
|
+
## Layout
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
native-app-wrapper/
|
|
73
|
+
├── bin/cli.js # init / configure / icons
|
|
74
|
+
├── profiles/ # one JSON file = one app identity (see profiles/README.md)
|
|
75
|
+
│ └── example.json # the template, and this package's own default
|
|
76
|
+
├── scripts/
|
|
77
|
+
│ ├── profile.mjs # loads + validates a profile; one definition of the schema
|
|
78
|
+
│ ├── configure.mjs # profile -> tauri.conf.json, generated_config.rs, remote.json
|
|
79
|
+
│ ├── build-sidecar.mjs # profile's sidecar.build -> src-tauri/binaries/<name>-<triple>
|
|
80
|
+
│ ├── generate-icons.mjs # profile's iconSource -> full src-tauri/icons/ set (Tauri CLI)
|
|
81
|
+
│ ├── generate-placeholder-icons.mjs # draws a buildable icon set with no artwork and no deps
|
|
82
|
+
│ └── lib/png.mjs # the PNG/ICO/ICNS writers that make the above possible
|
|
83
|
+
├── src-tauri/
|
|
84
|
+
│ ├── src/lib.rs # window setup, deep-link OAuth handoff, sidecar bridge, fullscreen toggle
|
|
85
|
+
│ ├── capabilities/ # Tauri's permission grants (default.json + a generated remote.json)
|
|
86
|
+
│ ├── icons/ # generated; checked in so a fresh clone builds
|
|
87
|
+
│ └── tauri.conf.json # generated by configure
|
|
88
|
+
├── assets/icon-source.png # the 1024px master `iconSource` points at by default
|
|
89
|
+
├── dist/ # the bundled frontend (a placeholder in remote mode; the app in local mode)
|
|
90
|
+
└── docs/
|
|
91
|
+
├── BUILDING.md # local dev/build commands and release builds
|
|
92
|
+
├── LOCAL_APPS.md # packaging a CLI: the sidecar bridge and the bundled frontend
|
|
93
|
+
├── OAUTH.md # why login needs a system browser + deep link, and how it works here
|
|
94
|
+
├── MOBILE.md # Android/iOS host requirements and gen/android, gen/apple
|
|
95
|
+
└── APP_STORES.md # Microsoft Store, Mac App Store, iOS App Store, Google Play
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Why generated config instead of a hand-edited `tauri.conf.json`
|
|
99
|
+
|
|
100
|
+
An app's identity shows up in five places that must agree: the Tauri config, the Rust deep-link
|
|
101
|
+
scheme, the Rust sidecar name, the remote capability's origin scope, and the icon set. Hand-editing
|
|
102
|
+
means five chances to drift. `scripts/configure.mjs` writes all of them from the profile, so the
|
|
103
|
+
profile is the only file anyone edits, and CI can assert the generated files still match it.
|
|
104
|
+
|
|
105
|
+
## Why a window, not a bundled copy of a remote site
|
|
106
|
+
|
|
107
|
+
In remote mode, `tauri.conf.json`'s main window points its `url` straight at the profile's site over
|
|
108
|
+
HTTPS — there's no local copy of the app's UI to keep in sync with the real site
|
|
109
|
+
(`build.frontendDist` still points at `dist/`, an unused placeholder Tauri's bundler requires to
|
|
110
|
+
exist). The tradeoff: the app needs network access to be useful, same as opening the site in a
|
|
111
|
+
browser tab would. Local mode is the opposite tradeoff — everything is bundled, nothing is fetched.
|
|
Binary file
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// native-app-wrapper CLI.
|
|
3
|
+
//
|
|
4
|
+
// init <dir> [--profile <name> | --profile-file <path>]
|
|
5
|
+
// Copy this wrapper into <dir> as a self-contained app, configured for
|
|
6
|
+
// one profile. The copy owns its profile from then on — nothing links
|
|
7
|
+
// back here — which is the point: a wrapped app's identity, icons, and
|
|
8
|
+
// build docs live next to the thing being wrapped, not in this package.
|
|
9
|
+
//
|
|
10
|
+
// configure [--profile <name>]
|
|
11
|
+
// Regenerate the Tauri config, Rust constants, and remote capability
|
|
12
|
+
// from a profile, in place. Run by the pre* npm scripts.
|
|
13
|
+
//
|
|
14
|
+
// icons [--profile <name>]
|
|
15
|
+
// Draw the dependency-free placeholder icon set (see
|
|
16
|
+
// scripts/generate-placeholder-icons.mjs); `npm run icons` instead
|
|
17
|
+
// generates a real set from the profile's iconSource via the Tauri CLI.
|
|
18
|
+
|
|
19
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { configure } from "../scripts/configure.mjs";
|
|
23
|
+
import { generatePlaceholderIcons } from "../scripts/generate-placeholder-icons.mjs";
|
|
24
|
+
import { loadProfile, resolveProfileName } from "../scripts/profile.mjs";
|
|
25
|
+
|
|
26
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
|
|
28
|
+
// What a scaffolded copy is made of. Generated artifacts (tauri.conf.json,
|
|
29
|
+
// generated_config.rs, src-tauri/icons/) are deliberately absent: `init`
|
|
30
|
+
// regenerates them for the new profile instead of copying this package's, so a
|
|
31
|
+
// copy can never inherit another app's identity by accident. Cargo.lock is
|
|
32
|
+
// absent for the same reason — it names this package's crate, and a copy that
|
|
33
|
+
// inherited it would fail its first build on a package/lock mismatch.
|
|
34
|
+
const SCAFFOLD_SOURCES = [
|
|
35
|
+
"bin",
|
|
36
|
+
"scripts",
|
|
37
|
+
"src-tauri/Cargo.toml",
|
|
38
|
+
"src-tauri/build.rs",
|
|
39
|
+
"src-tauri/src/lib.rs",
|
|
40
|
+
"src-tauri/src/main.rs",
|
|
41
|
+
"src-tauri/capabilities/default.json",
|
|
42
|
+
"dist",
|
|
43
|
+
".gitignore",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Which docs a copy gets depends on what its profile actually does — an
|
|
47
|
+
// offline CLI app shipping a guide to browser OAuth handoffs is noise that
|
|
48
|
+
// reads as unimplemented functionality.
|
|
49
|
+
function docsFor(profile) {
|
|
50
|
+
const docs = ["docs/BUILDING.md", "docs/APP_STORES.md"];
|
|
51
|
+
if (profile.mode === "local") docs.push("docs/LOCAL_APPS.md");
|
|
52
|
+
if (profile.deepLinkScheme) docs.push("docs/OAUTH.md");
|
|
53
|
+
if (profile.android || profile.ios) docs.push("docs/MOBILE.md");
|
|
54
|
+
return docs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Same idea for the npm scripts: a desktop-only profile's package.json
|
|
58
|
+
// shouldn't offer android:build.
|
|
59
|
+
function scriptsFor(profile, template) {
|
|
60
|
+
const scripts = { ...template };
|
|
61
|
+
if (!profile.android && !profile.ios) {
|
|
62
|
+
for (const key of Object.keys(scripts)) {
|
|
63
|
+
if (/^(pre)?(android|ios):/.test(key)) delete scripts[key];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (profile.sidecar) {
|
|
67
|
+
// The bundler fails outright when no sidecar binary exists for the target
|
|
68
|
+
// triple, so building it is part of running or building the app, not a
|
|
69
|
+
// step to remember.
|
|
70
|
+
scripts["build:sidecar"] = "node scripts/build-sidecar.mjs";
|
|
71
|
+
scripts.predev = "node bin/cli.js configure && node scripts/build-sidecar.mjs";
|
|
72
|
+
scripts["prebuild:desktop"] = "node bin/cli.js configure && node scripts/build-sidecar.mjs";
|
|
73
|
+
} else {
|
|
74
|
+
delete scripts["build:sidecar"];
|
|
75
|
+
}
|
|
76
|
+
delete scripts.init;
|
|
77
|
+
return scripts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function flag(argv, name) {
|
|
81
|
+
const i = argv.indexOf(name);
|
|
82
|
+
if (i === -1) return undefined;
|
|
83
|
+
const value = argv[i + 1];
|
|
84
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} needs a value`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fail(message) {
|
|
89
|
+
console.error(`[native-app-wrapper] ${message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slug(text) {
|
|
94
|
+
return text
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
97
|
+
.replace(/^-|-$/g, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Splits argv into positionals and flags, treating `--profile`/`--profile-file` as value flags. */
|
|
101
|
+
function positionals(argv) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (let i = 0; i < argv.length; i++) {
|
|
104
|
+
if (argv[i] === "--profile" || argv[i] === "--profile-file") {
|
|
105
|
+
i++; // skip the flag's value
|
|
106
|
+
} else if (!argv[i].startsWith("--")) {
|
|
107
|
+
out.push(argv[i]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function init(argv) {
|
|
114
|
+
const [targetArg] = positionals(argv);
|
|
115
|
+
if (!targetArg) fail("init needs a target directory: init <dir> [--profile-file <path>]");
|
|
116
|
+
|
|
117
|
+
const targetDir = path.resolve(process.cwd(), targetArg);
|
|
118
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !argv.includes("--force")) {
|
|
119
|
+
fail(`${targetDir} already exists and is not empty — pass --force to overwrite its wrapper files`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const profileFile = flag(argv, "--profile-file");
|
|
123
|
+
const profileName = profileFile
|
|
124
|
+
? path.basename(profileFile).replace(/\.json$/, "")
|
|
125
|
+
: (flag(argv, "--profile") ?? "example");
|
|
126
|
+
|
|
127
|
+
if (!profileFile && !existsSync(path.join(packageRoot, "profiles", `${profileName}.json`))) {
|
|
128
|
+
fail(`no profile named "${profileName}" in ${path.join(packageRoot, "profiles")}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const stagedProfilePath = profileFile
|
|
132
|
+
? path.resolve(process.cwd(), profileFile)
|
|
133
|
+
: path.join(packageRoot, "profiles", `${profileName}.json`);
|
|
134
|
+
|
|
135
|
+
mkdirSync(targetDir, { recursive: true });
|
|
136
|
+
for (const entry of SCAFFOLD_SOURCES) {
|
|
137
|
+
const from = path.join(packageRoot, entry);
|
|
138
|
+
if (!existsSync(from)) continue;
|
|
139
|
+
const to = path.join(targetDir, entry);
|
|
140
|
+
mkdirSync(path.dirname(to), { recursive: true });
|
|
141
|
+
cpSync(from, to, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The copy gets exactly one profile — its own — so its scripts resolve it
|
|
145
|
+
// with no --profile flag and no ambiguity about which app this tree is.
|
|
146
|
+
mkdirSync(path.join(targetDir, "profiles"), { recursive: true });
|
|
147
|
+
cpSync(stagedProfilePath, path.join(targetDir, "profiles", `${profileName}.json`));
|
|
148
|
+
cpSync(path.join(packageRoot, "profiles", "README.md"), path.join(targetDir, "profiles", "README.md"));
|
|
149
|
+
|
|
150
|
+
const profile = loadProfile(targetDir, profileName);
|
|
151
|
+
|
|
152
|
+
for (const doc of docsFor(profile)) {
|
|
153
|
+
const to = path.join(targetDir, doc);
|
|
154
|
+
mkdirSync(path.dirname(to), { recursive: true });
|
|
155
|
+
cpSync(path.join(packageRoot, doc), to);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// package.json is rewritten rather than copied: the copy is a distinct app
|
|
159
|
+
// with its own name and version, and it must not carry this package's.
|
|
160
|
+
const template = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
161
|
+
writeFileSync(
|
|
162
|
+
path.join(targetDir, "package.json"),
|
|
163
|
+
`${JSON.stringify(
|
|
164
|
+
{
|
|
165
|
+
...template,
|
|
166
|
+
name: `${slug(profile.appName)}-native`,
|
|
167
|
+
version: profile.version,
|
|
168
|
+
description: `${profile.appName} packaged as a native desktop app (scaffolded from native-app-wrapper)`,
|
|
169
|
+
private: true,
|
|
170
|
+
bin: undefined,
|
|
171
|
+
scripts: scriptsFor(profile, template.scripts),
|
|
172
|
+
},
|
|
173
|
+
null,
|
|
174
|
+
2,
|
|
175
|
+
)}\n`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const { written } = configure(targetDir, profileName);
|
|
179
|
+
const icons = generatePlaceholderIcons(targetDir, {
|
|
180
|
+
background: ["#1e293b", "#0b1220"],
|
|
181
|
+
accent: "#f8fafc",
|
|
182
|
+
...profile.placeholderIcon,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// src-tauri/Cargo.toml's crate name is what `tauri build` names the binary,
|
|
186
|
+
// so it has to follow the copy rather than stay "native-app-wrapper".
|
|
187
|
+
const crateName = `${slug(profile.appName)}-native`;
|
|
188
|
+
const cargoPath = path.join(targetDir, "src-tauri", "Cargo.toml");
|
|
189
|
+
writeFileSync(
|
|
190
|
+
cargoPath,
|
|
191
|
+
readFileSync(cargoPath, "utf8")
|
|
192
|
+
.replace(/^name = "native-app-wrapper"$/m, `name = "${crateName}"`)
|
|
193
|
+
.replace(/^name = "native_app_wrapper_lib"$/m, `name = "${crateName.replace(/-/g, "_")}_lib"`)
|
|
194
|
+
.replace(/^description = ".*"$/m, `description = "${profile.appName} packaged as a native app"`),
|
|
195
|
+
"utf8",
|
|
196
|
+
);
|
|
197
|
+
const mainPath = path.join(targetDir, "src-tauri", "src", "main.rs");
|
|
198
|
+
writeFileSync(
|
|
199
|
+
mainPath,
|
|
200
|
+
readFileSync(mainPath, "utf8").replace("native_app_wrapper_lib::run()", `${crateName.replace(/-/g, "_")}_lib::run()`),
|
|
201
|
+
"utf8",
|
|
202
|
+
);
|
|
203
|
+
const rel = path.relative(process.cwd(), targetDir) || ".";
|
|
204
|
+
console.log(`[native-app-wrapper] scaffolded ${profile.appName} into ${rel}/`);
|
|
205
|
+
for (const entry of written) console.log(` wrote ${entry}`);
|
|
206
|
+
console.log(` wrote ${icons.length} placeholder icons into src-tauri/icons/`);
|
|
207
|
+
console.log(`\nNext: cd ${rel} && npm install && npm run dev`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const [command, ...argv] = process.argv.slice(2);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
if (command === "init") {
|
|
214
|
+
init(argv);
|
|
215
|
+
} else if (command === "configure") {
|
|
216
|
+
const rootDir = packageRoot;
|
|
217
|
+
const { profile, written } = configure(rootDir, resolveProfileName(rootDir, argv));
|
|
218
|
+
const target = profile.mode === "remote" ? profile.url : "bundled dist/ frontend";
|
|
219
|
+
console.log(`[native-app-wrapper] configured "${profile.name}" (${profile.appName} -> ${target})`);
|
|
220
|
+
for (const entry of written) console.log(` wrote ${entry}`);
|
|
221
|
+
} else if (command === "icons") {
|
|
222
|
+
const profile = loadProfile(packageRoot, resolveProfileName(packageRoot, argv));
|
|
223
|
+
const written = generatePlaceholderIcons(packageRoot, {
|
|
224
|
+
background: ["#1e293b", "#0b1220"],
|
|
225
|
+
accent: "#f8fafc",
|
|
226
|
+
...profile.placeholderIcon,
|
|
227
|
+
});
|
|
228
|
+
console.log(`[native-app-wrapper] drew ${written.length} placeholder icons for "${profile.name}"`);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(
|
|
231
|
+
[
|
|
232
|
+
"native-app-wrapper",
|
|
233
|
+
"",
|
|
234
|
+
" init <dir> [--profile <name>] [--profile-file <path>] [--force]",
|
|
235
|
+
" Copy this wrapper into <dir> as a standalone app for one profile.",
|
|
236
|
+
"",
|
|
237
|
+
" configure [--profile <name>]",
|
|
238
|
+
" Regenerate tauri.conf.json, the Rust constants, and the remote capability.",
|
|
239
|
+
"",
|
|
240
|
+
" icons [--profile <name>]",
|
|
241
|
+
" Draw the placeholder icon set (no dependencies, no artwork needed).",
|
|
242
|
+
].join("\n"),
|
|
243
|
+
);
|
|
244
|
+
process.exit(command ? 1 : 0);
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
fail(error.message);
|
|
248
|
+
}
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Loading…</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body { height: 100%; margin: 0; background: #0b1220; color: #d8d8d8; font-family: system-ui, sans-serif; }
|
|
8
|
+
body { display: flex; align-items: center; justify-content: center; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<!--
|
|
13
|
+
In remote mode this file is never shown: the main window's `url` in
|
|
14
|
+
tauri.conf.json points straight at the configured site. Tauri's bundler
|
|
15
|
+
still requires `build.frontendDist` to point at a real, non-empty
|
|
16
|
+
directory, and this doubles as the offline fallback content a future
|
|
17
|
+
retry/offline screen can build on.
|
|
18
|
+
|
|
19
|
+
In local mode this directory IS the app — replace this file with the
|
|
20
|
+
frontend you want bundled. See ../docs/LOCAL_APPS.md.
|
|
21
|
+
-->
|
|
22
|
+
<p>Loading…</p>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Submitting to app stores
|
|
2
|
+
|
|
3
|
+
This covers what each store needs beyond the installers `npm run build:desktop` (or a CI matrix
|
|
4
|
+
like the one in `BUILDING.md`) already produces.
|
|
5
|
+
None of this is automated — every store below needs an account, credentials only a human on that
|
|
6
|
+
account can provision, and (for Apple) manual review — so treat this as a checklist, not a script.
|
|
7
|
+
|
|
8
|
+
## Microsoft Store (Windows)
|
|
9
|
+
|
|
10
|
+
Tauri does not build MSIX — it only produces the `.exe` (NSIS) and `.msi` (WiX) installers
|
|
11
|
+
`build:desktop` already produces. The Microsoft Store submission type for that is an **"EXE or MSI
|
|
12
|
+
app"**: the Store hosts a listing that links to your own signed installer rather than a sandboxed
|
|
13
|
+
MSIX package.
|
|
14
|
+
|
|
15
|
+
1. Build with a Store-specific config overlay that forces the offline WebView2 installer (Store
|
|
16
|
+
review rejects the online/evergreen bootstrapper) and code-sign the installer:
|
|
17
|
+
```jsonc
|
|
18
|
+
// tauri.microsoftstore.conf.json (create this file, merged over tauri.conf.json)
|
|
19
|
+
{ "bundle": { "windows": { "webviewInstallMode": { "type": "offlineInstaller" } } } }
|
|
20
|
+
```
|
|
21
|
+
```bash
|
|
22
|
+
npx tauri build --config tauri.microsoftstore.conf.json
|
|
23
|
+
```
|
|
24
|
+
2. In [Partner Center](https://partner.microsoft.com/dashboard), register the app under
|
|
25
|
+
**Apps and Games → EXE or MSI app**, using a **Publisher name distinct from the product name**
|
|
26
|
+
(Store review flags the two matching exactly), and supply the installer's silent-install flag
|
|
27
|
+
(NSIS's `/S`, applied automatically by Tauri's installer).
|
|
28
|
+
3. Submit for certification. Because the Store links to an external installer, your own
|
|
29
|
+
update mechanism (Tauri's updater, once enabled — see `BUILDING.md`) is what actually
|
|
30
|
+
updates users; the Store doesn't auto-update this install type the way it would a native MSIX.
|
|
31
|
+
|
|
32
|
+
A true sandboxed MSIX is possible but needs a separate third-party packaging step on top of
|
|
33
|
+
Tauri's own bundler (e.g. the community `@choochmeque/tauri-windows-bundle` tool) — not something
|
|
34
|
+
this wrapper sets up by default.
|
|
35
|
+
|
|
36
|
+
## Mac App Store
|
|
37
|
+
|
|
38
|
+
Distinct from notarized-`.dmg` distribution outside the Store (which uses a Developer ID cert and
|
|
39
|
+
`notarytool`) — App Store distribution needs a **3rd Party Mac Developer Application** cert, a
|
|
40
|
+
**3rd Party Mac Developer Installer** cert, **App Sandbox entitlements**, and upload as a signed
|
|
41
|
+
`.pkg`, not a `.dmg`.
|
|
42
|
+
|
|
43
|
+
1. `src-tauri/Entitlements.plist` (create this; not generated by `configure`) needs at least:
|
|
44
|
+
```xml
|
|
45
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
46
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
47
|
+
<plist version="1.0">
|
|
48
|
+
<dict>
|
|
49
|
+
<key>com.apple.security.app-sandbox</key><true/>
|
|
50
|
+
<key>com.apple.security.network.client</key><true/>
|
|
51
|
+
<!-- Tauri's webview needs JIT; omitting this is a documented cause of
|
|
52
|
+
crash-on-launch after notarization. -->
|
|
53
|
+
<key>com.apple.security.cs.allow-jit</key><true/>
|
|
54
|
+
<key>com.apple.application-identifier</key><string>$TEAM_ID.$IDENTIFIER</string>
|
|
55
|
+
<key>com.apple.developer.team-identifier</key><string>$TEAM_ID</string>
|
|
56
|
+
</dict>
|
|
57
|
+
</plist>
|
|
58
|
+
```
|
|
59
|
+
and reference it from `tauri.conf.json`'s `bundle.macOS.entitlements`.
|
|
60
|
+
2. Build, sign, and upload:
|
|
61
|
+
```bash
|
|
62
|
+
npx tauri build --bundles app --target universal-apple-darwin
|
|
63
|
+
xcrun productbuild --sign "3rd Party Mac Developer Installer: <Your Team> (TEAMID)" \
|
|
64
|
+
--component "src-tauri/target/universal-apple-darwin/release/bundle/macos/<Product Name>.app" \
|
|
65
|
+
/Applications <Product>.pkg
|
|
66
|
+
xcrun altool --upload-app --type macos --file <Product>.pkg \
|
|
67
|
+
--apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER"
|
|
68
|
+
```
|
|
69
|
+
(Apple has been steering uploads toward the newer Transporter CLI / `notarytool`-adjacent
|
|
70
|
+
tooling; verify `altool`'s status against Apple's current documentation before relying on it.)
|
|
71
|
+
3. In [App Store Connect](https://appstoreconnect.apple.com), register the app with a Bundle ID
|
|
72
|
+
matching `tauri.conf.json`'s `identifier` exactly, create a **Mac App Store** provisioning
|
|
73
|
+
profile (distinct from a Developer ID profile), and generate an API key (`.p8`) for CI/CLI
|
|
74
|
+
auth.
|
|
75
|
+
|
|
76
|
+
## iOS App Store
|
|
77
|
+
|
|
78
|
+
1. Build a signed `.ipa`: `npm run ios:build -- --export-method app-store-connect` (needs a
|
|
79
|
+
distribution certificate + provisioning profile configured in the generated Xcode project
|
|
80
|
+
first — see `MOBILE.md`). Output lands at
|
|
81
|
+
`src-tauri/gen/apple/build/arm64/*.ipa`.
|
|
82
|
+
2. Upload the same way as macOS: `xcrun altool --upload-app --type ios --file *.ipa --apiKey ... --apiIssuer ...`,
|
|
83
|
+
or drag it into Xcode Organizer / the Transporter app. It lands in App Store Connect →
|
|
84
|
+
**TestFlight** automatically — internal testers get it immediately, external testers need a
|
|
85
|
+
lightweight Beta App Review first.
|
|
86
|
+
3. **Read this before investing in an iOS submission**: Apple's App Store Review Guideline
|
|
87
|
+
**4.2 "Minimum Functionality"** is a real rejection risk for an app that's essentially "load a
|
|
88
|
+
website in a webview" with no native chrome —
|
|
89
|
+
> "Your app should include features, content, and UI that elevate it beyond a repackaged
|
|
90
|
+
> website... Other than catalogs, apps shouldn't primarily be marketing materials,
|
|
91
|
+
> advertisements, web clippings, content aggregators, or a collection of links."
|
|
92
|
+
([developer.apple.com/app-store/review/guidelines](https://developer.apple.com/app-store/review/guidelines/), §4.2)
|
|
93
|
+
|
|
94
|
+
iOS reviewers test submissions manually and are noticeably stricter here than Android or
|
|
95
|
+
desktop review. To materially reduce rejection risk before submitting, this app needs to be
|
|
96
|
+
more than a URL in a box: native navigation chrome (a real tab bar / back gesture, not just
|
|
97
|
+
in-page nav), push notifications wired to APNs, a native offline-state screen instead of a
|
|
98
|
+
blank WebView on connectivity loss — enough that the app doesn't read as "open Safari to this
|
|
99
|
+
URL." This is a product/design decision for whoever owns the app, not a packaging setting —
|
|
100
|
+
flag it before committing engineering time to an iOS submission specifically.
|
|
101
|
+
|
|
102
|
+
## Google Play Store (Android)
|
|
103
|
+
|
|
104
|
+
1. **Format**: Google requires the **Android App Bundle** (`.aab`, not a plain `.apk`) for new
|
|
105
|
+
apps — `npm run android:build` produces one; a CI Android job can build it (see `MOBILE.md`).
|
|
106
|
+
2. **Play App Signing** is mandatory for new apps on AAB: Google holds the app signing key, you
|
|
107
|
+
keep only an upload key. Enroll during your first release in
|
|
108
|
+
[Play Console](https://play.google.com/console).
|
|
109
|
+
3. **Target API level** (current requirements as of this writing):
|
|
110
|
+
- New apps/updates must target **Android 16 (API 36)** by **August 31, 2026** (extension to
|
|
111
|
+
November 1, 2026 available on request in Play Console). The profile's
|
|
112
|
+
`android.minSdkVersion` is the *minimum* supported OS, unrelated to the *target* level —
|
|
113
|
+
set the target via `gen/android`'s Gradle config once you run `android:init`.
|
|
114
|
+
- Existing published apps need at least **API 35** to stay visible to users on newer Android
|
|
115
|
+
versions.
|
|
116
|
+
- Build with **NDK r28+** (see `MOBILE.md`) — Play now requires 16KB-page-aligned native
|
|
117
|
+
libraries for new apps/updates targeting recent devices, which NDK ≥28 produces automatically.
|
|
118
|
+
4. Submission flow: create the app listing, enable Play App Signing, upload the signed `.aab` to
|
|
119
|
+
an internal/closed testing track first, complete the **Data Safety** form and content-rating
|
|
120
|
+
questionnaire (a remote-mode app relays OAuth data through its login handoff, and a local-mode app may read
|
|
121
|
+
system state — describe whichever applies accurately), then promote to production. Play has no equivalent to Apple's 4.2 guideline
|
|
122
|
+
strictly enforced against webview wrappers.
|
|
123
|
+
|
|
124
|
+
## GitHub Releases (no store at all)
|
|
125
|
+
|
|
126
|
+
The default, and what `BUILDING.md`'s CI matrix produces: desktop installers attach directly to a
|
|
127
|
+
GitHub Release. No review process, no developer-account fees, works today — the tradeoff is no store discoverability and users see an
|
|
128
|
+
"unidentified developer" / SmartScreen warning on first launch unless the signing secrets above
|
|
129
|
+
are configured.
|
package/docs/BUILDING.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Building
|
|
2
|
+
|
|
3
|
+
Every command below is run from the app directory — either this package (which builds
|
|
4
|
+
`profiles/example.json` as a smoke test) or a copy `node bin/cli.js init` scaffolded elsewhere.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
Tauri compiles a Rust binary against the host's native webview, so each OS needs its own toolchain
|
|
9
|
+
before anything here works. From [Tauri's prerequisites guide](https://v2.tauri.app/start/prerequisites/):
|
|
10
|
+
|
|
11
|
+
| Host | Needs |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Windows** | [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) (Desktop development with C++), WebView2 (preinstalled on Windows 10 1803+/11), and Rust. |
|
|
14
|
+
| **macOS** | Xcode Command Line Tools (`xcode-select --install`) and Rust. |
|
|
15
|
+
| **Linux** | `libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev build-essential curl wget file libssl-dev` (Debian/Ubuntu names; see the guide for Fedora/Arch), and Rust. |
|
|
16
|
+
|
|
17
|
+
Plus [Rust](https://rustup.rs) 1.77.2 or newer and Node 18+ everywhere.
|
|
18
|
+
|
|
19
|
+
## Local dev
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`predev` regenerates the Tauri config from the profile first, so a profile edit takes effect
|
|
27
|
+
without a separate step. In remote mode the window opens on the profile's `url` — there's no local
|
|
28
|
+
frontend to serve, so a dev server for the wrapped site is unrelated (to test against one, point
|
|
29
|
+
the profile's `url` at `http://localhost:3000` temporarily, re-run `npm run configure`, and don't
|
|
30
|
+
commit that change). In local mode the window opens on `dist/`.
|
|
31
|
+
|
|
32
|
+
## Release build
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run build:desktop
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output lands under `src-tauri/target/release/bundle/`:
|
|
39
|
+
|
|
40
|
+
| Platform | Artifacts |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Windows | `msi/*.msi` (WiX), `nsis/*-setup.exe` |
|
|
43
|
+
| macOS | `dmg/*.dmg`, `macos/*.app` |
|
|
44
|
+
| Linux | `deb/*.deb`, `rpm/*.rpm`, `appimage/*.AppImage` |
|
|
45
|
+
|
|
46
|
+
**Each installer must be built on its own OS.** Tauri does not cross-compile: a Windows `.msi`
|
|
47
|
+
needs a Windows host, a `.dmg` needs macOS. That's what makes a CI matrix (below) the normal way to
|
|
48
|
+
produce a full set.
|
|
49
|
+
|
|
50
|
+
macOS builds default to the host's architecture; add `--target universal-apple-darwin` (and
|
|
51
|
+
`rustup target add x86_64-apple-darwin aarch64-apple-darwin`) for a universal binary that runs on
|
|
52
|
+
both Intel and Apple Silicon.
|
|
53
|
+
|
|
54
|
+
If the profile bundles a sidecar, build it **before** `build:desktop` — the bundle step fails
|
|
55
|
+
outright when no binary exists for the target triple. See `LOCAL_APPS.md`.
|
|
56
|
+
|
|
57
|
+
## Android / iOS
|
|
58
|
+
|
|
59
|
+
See `MOBILE.md` — needs host tooling this package can't install for you. Sidecars are desktop-only,
|
|
60
|
+
so a local-mode app with a `sidecar` has no mobile story; a remote-mode app does.
|
|
61
|
+
|
|
62
|
+
## CI
|
|
63
|
+
|
|
64
|
+
Building for three OSes means three runners. The shape that works:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
strategy:
|
|
68
|
+
matrix:
|
|
69
|
+
include:
|
|
70
|
+
- { os: windows-latest, args: "" }
|
|
71
|
+
- { os: macos-latest, args: "--target universal-apple-darwin" }
|
|
72
|
+
- { os: ubuntu-22.04, args: "" }
|
|
73
|
+
runs-on: ${{ matrix.os }}
|
|
74
|
+
steps:
|
|
75
|
+
# ... checkout, node, rust, and the Linux apt packages from the table above ...
|
|
76
|
+
- uses: tauri-apps/tauri-action@v0
|
|
77
|
+
with:
|
|
78
|
+
projectPath: <path to the app directory>
|
|
79
|
+
args: ${{ matrix.args }}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
[`tauri-apps/tauri-action`](https://github.com/tauri-apps/tauri-action) handles the build and can
|
|
83
|
+
attach the artifacts to a GitHub Release. `packages/about-system-info`'s
|
|
84
|
+
`.github/workflows/about-system-desktop.yml` is a complete working instance of this, including the
|
|
85
|
+
sidecar build step that has to run first.
|
|
86
|
+
|
|
87
|
+
Worth asserting in CI: that `src-tauri/tauri.conf.json` and `src-tauri/src/generated_config.rs`
|
|
88
|
+
still match what `configure` produces from the profile. Re-running configure and checking
|
|
89
|
+
`git diff --exit-code` catches hand-edits that have drifted from the profile they're generated
|
|
90
|
+
from.
|
|
91
|
+
|
|
92
|
+
## Code signing
|
|
93
|
+
|
|
94
|
+
Unsigned installers work, but users get an "unidentified developer" (macOS) or SmartScreen
|
|
95
|
+
(Windows) warning on first launch. `tauri-action` picks up signing automatically once the relevant
|
|
96
|
+
secrets exist:
|
|
97
|
+
|
|
98
|
+
- **macOS**: `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, and for
|
|
99
|
+
notarization `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` (or `APPLE_API_KEY`,
|
|
100
|
+
`APPLE_API_ISSUER`, `APPLE_API_KEY_PATH`).
|
|
101
|
+
- **Windows**: an Authenticode certificate, configured through `bundle.windows.certificateThumbprint`
|
|
102
|
+
or a signing command.
|
|
103
|
+
|
|
104
|
+
See `APP_STORES.md` for where those credentials come from.
|
|
105
|
+
|
|
106
|
+
## Auto-update (off by default)
|
|
107
|
+
|
|
108
|
+
`tauri-plugin-updater` is in `Cargo.toml`, but a profile with no `updater` block never registers
|
|
109
|
+
it and `tauri.conf.json` gets no `plugins.updater` section. That is not the same as switching it
|
|
110
|
+
off with `"active": false` — the plugin reads `pubkey` during *initialization* and panics if it
|
|
111
|
+
isn't there, so an inert-looking config block is a crash on launch, not a disabled feature.
|
|
112
|
+
|
|
113
|
+
To turn updates on:
|
|
114
|
+
|
|
115
|
+
1. `npx @tauri-apps/cli@2 signer generate` — keep the private key, note the public key.
|
|
116
|
+
2. Add to the profile:
|
|
117
|
+
```json
|
|
118
|
+
"updater": {
|
|
119
|
+
"pubkey": "<the public key>",
|
|
120
|
+
"endpoints": ["https://github.com/<owner>/<repo>/releases/latest/download/latest.json"]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
and re-run `configure`. (Both fields are required; the profile validator rejects one without
|
|
124
|
+
the other.)
|
|
125
|
+
3. Store the private key as a `TAURI_SIGNING_PRIVATE_KEY` secret, and its password as
|
|
126
|
+
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if you set one.
|
|
127
|
+
4. Set `tauri-action`'s `includeUpdaterJson: true` so releases carry the `latest.json` manifest
|
|
128
|
+
the updater polls.
|
|
129
|
+
|
|
130
|
+
## Cutting a release
|
|
131
|
+
|
|
132
|
+
Bump `version` in the profile, run `npm run configure`, commit, then tag. The version in
|
|
133
|
+
`tauri.conf.json` is what ends up in the installer's metadata and in every store listing, so the
|
|
134
|
+
profile is the single place it's set.
|