@inglorious/capacitor 0.3.0 → 0.4.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 CHANGED
@@ -5,6 +5,7 @@
5
5
  Native project setup for [Capacitor](https://capacitorjs.com) apps, written once in `package.json` instead of clicked through Xcode and Android Studio. `@inglorious/create-app --mobile` sets it up for you.
6
6
 
7
7
  - `inglorious-capacitor configure` writes the app's native settings into `android/` and `ios/` after every `cap sync`: App Links and Universal Links, the signing team, push, Analytics off until consent and without the advertising ID, permissions and Info.plist entries, and the Firebase files.
8
+ - `inglorious-capacitor dev <ios|android>` runs the app on a device or emulator with live reload from the Vite dev server.
8
9
  - `inglorious-capacitor assets` makes every icon and splash screen, light and dark, from one logo.
9
10
  - `inglorious-capacitor listing-assets` captures store screenshots of the running app in every size and language, and makes the promotional images (Google Play's feature graphic, a header and a search results card) and Google Play's 512px icon.
10
11
 
@@ -22,6 +23,8 @@ Run `configure` as Capacitor's `sync` hook, so `cap add` and `cap sync` both app
22
23
  "capacitor:sync:after": "inglorious-capacitor configure",
23
24
  "mobile:init:ios": "cap add ios && inglorious-capacitor assets --ios",
24
25
  "mobile:init:android": "cap add android && inglorious-capacitor assets --android",
26
+ "mobile:dev:ios": "inglorious-capacitor dev ios",
27
+ "mobile:dev:android": "inglorious-capacitor dev android",
25
28
  "mobile:assets": "inglorious-capacitor assets",
26
29
  "mobile:listing-assets": "inglorious-capacitor listing-assets"
27
30
  }
@@ -105,6 +108,10 @@ It runs for the platform Capacitor is syncing, and changes nothing when the sett
105
108
 
106
109
  It centres the logo (`public/logo.svg` or `public/logo.png` by default) on `background` for the icons and the splash screen, and on `backgroundDark` for the dark splash screen, then hands the result to [`@capacitor/assets`](https://github.com/ionic-team/capacitor-assets) for every size and density. `iconScale` and `splashScale` are the logo's width as a share of the icon and of the splash screen. Pass `--ios` or `--android` for one platform.
107
110
 
111
+ ## Live reload on a device
112
+
113
+ `inglorious-capacitor dev ios` (or `android`) starts Vite on the local network (`vite --host`) and runs `cap run ios --live-reload`, which points the installed app at it: every saved file reloads on the phone. The phone must be on the same network as the computer. It builds the web app first if `webDir` has none, since `cap run` syncs it. Further options go to `cap run`, such as `--target <id>`; `--port=<port>` changes Vite's port (5173 by default). Ctrl+C stops both, and Capacitor puts the native config back.
114
+
108
115
  ## Store listing images
109
116
 
110
117
  `listing-assets` opens the running app in your installed Chrome (through `playwright-core`, so no browser download) and captures the screens listed in `listing.config.js`, once per device and language:
@@ -5,6 +5,9 @@ const USAGE = `Usage: inglorious-capacitor <command>
5
5
 
6
6
  configure Apply the "native" settings in package.json to the
7
7
  native projects (runs after every cap sync)
8
+ dev <ios|android> [--port=5173] [cap run options]
9
+ Run the app on a device or emulator with live reload
10
+ from the Vite dev server
8
11
  assets [--ios] [--android]
9
12
  Make the app icons and splash screens from the logo
10
13
  listing-assets Capture store screenshots of the running app, and make
@@ -21,6 +24,15 @@ try {
21
24
  const { configure } = await import("../src/configure/index.js")
22
25
  const platforms = await configure()
23
26
  if (platforms.length) console.log(`Configured ${platforms.join(" and ")}`)
27
+ } else if (command === "dev") {
28
+ const { dev } = await import("../src/dev/index.js")
29
+ const [platform, ...rest] = flags
30
+ const portFlag = rest.find((flag) => flag.startsWith("--port="))
31
+ process.exitCode = await dev({
32
+ platform,
33
+ port: portFlag ? Number(portFlag.slice("--port=".length)) : undefined,
34
+ args: rest.filter((flag) => flag !== portFlag),
35
+ })
24
36
  } else if (command === "assets") {
25
37
  const { assets } = await import("../src/assets/index.js")
26
38
  await assets({ platforms: flags.map((flag) => flag.replace(/^--/, "")) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/capacitor",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Configures Capacitor's native projects from package.json, makes their icons and splash screens from one logo, and the screenshots and graphics the stores ask for.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
package/src/config.js CHANGED
@@ -41,6 +41,7 @@ export async function readSettings(root, env = process.env) {
41
41
  root,
42
42
  appId: capacitor.appId,
43
43
  appName: capacitor.appName,
44
+ webDir: capacitor.webDir ?? "www",
44
45
  appLinks: native.appLinks?.domain
45
46
  ? normalizeAppLinks(native.appLinks)
46
47
  : null,
@@ -0,0 +1,103 @@
1
+ import { EventEmitter } from "node:events"
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
7
+
8
+ import { dev } from "./index.js"
9
+
10
+ const CAPACITOR_CONFIG = {
11
+ appId: "com.example.app",
12
+ appName: "Example",
13
+ webDir: "dist",
14
+ }
15
+
16
+ describe("dev", () => {
17
+ let root
18
+
19
+ beforeEach(() => {
20
+ root = mkdtempSync(path.join(tmpdir(), "inglorious-capacitor-dev-"))
21
+ writeFileSync(path.join(root, "package.json"), "{}")
22
+ writeFileSync(
23
+ path.join(root, "capacitor.config.json"),
24
+ JSON.stringify(CAPACITOR_CONFIG),
25
+ )
26
+ })
27
+
28
+ afterEach(() => {
29
+ rmSync(root, { recursive: true, force: true })
30
+ })
31
+
32
+ it("serves Vite on the network and runs the app against it", async () => {
33
+ buildWebDir(root)
34
+ const spawn = fakeSpawn()
35
+
36
+ const code = await dev({
37
+ root,
38
+ platform: "ios",
39
+ args: ["--target", "ABC"],
40
+ spawn,
41
+ isReady: async () => true,
42
+ })
43
+
44
+ expect(code).toBe(0)
45
+ expect(spawn.calls).toEqual([
46
+ ["vite", ["--host", "--port", "5173", "--strictPort"]],
47
+ [
48
+ "cap",
49
+ ["run", "ios", "--live-reload", "--port", "5173", "--target", "ABC"],
50
+ ],
51
+ ])
52
+ expect(spawn.children[0].kill).toHaveBeenCalled()
53
+ })
54
+
55
+ it("builds the web app first when it was never built", async () => {
56
+ const spawn = fakeSpawn()
57
+
58
+ await dev({
59
+ root,
60
+ platform: "android",
61
+ port: 3000,
62
+ spawn,
63
+ isReady: async () => true,
64
+ })
65
+
66
+ expect(
67
+ spawn.calls.map(([command, args]) => `${command} ${args[0]}`),
68
+ ).toEqual(["vite build", "vite --host", "cap run"])
69
+ })
70
+
71
+ it("stops the dev server when the app fails", async () => {
72
+ buildWebDir(root)
73
+ const spawn = fakeSpawn({ cap: 1 })
74
+
75
+ await expect(
76
+ dev({ root, platform: "android", spawn, isReady: async () => true }),
77
+ ).rejects.toThrow("exited with 1")
78
+ expect(spawn.children[0].kill).toHaveBeenCalled()
79
+ })
80
+
81
+ it("refuses an unknown platform", async () => {
82
+ await expect(dev({ root, platform: "web" })).rejects.toThrow("Usage")
83
+ })
84
+ })
85
+
86
+ function buildWebDir(root) {
87
+ mkdirSync(path.join(root, "dist"))
88
+ writeFileSync(path.join(root, "dist", "index.html"), "")
89
+ }
90
+
91
+ function fakeSpawn(exitCodes = {}) {
92
+ const spawn = (command, args) => {
93
+ spawn.calls.push([command, args])
94
+ const child = Object.assign(new EventEmitter(), { kill: vi.fn() })
95
+ spawn.children.push(child)
96
+ const isServer = command === "vite" && args[0] === "--host"
97
+ if (!isServer) setTimeout(() => child.emit("exit", exitCodes[command] ?? 0))
98
+ return child
99
+ }
100
+ spawn.calls = []
101
+ spawn.children = []
102
+ return spawn
103
+ }
@@ -0,0 +1,101 @@
1
+ import { spawn as nodeSpawn } from "node:child_process"
2
+ import { existsSync } from "node:fs"
3
+ import path from "node:path"
4
+ import process from "node:process"
5
+
6
+ import { readSettings } from "../config.js"
7
+
8
+ export const DEV_PORT = 5173
9
+ const PLATFORMS = ["ios", "android"]
10
+ const SUCCESS = 0
11
+ const READY_POLL_MS = 250
12
+ const READY_TIMEOUT_MS = 30_000
13
+
14
+ /**
15
+ * Runs the app on a phone or emulator against the Vite dev server on this
16
+ * computer, so every saved file reloads on the device. Vite listens on the
17
+ * local network and `cap run --live-reload` points the native app at it;
18
+ * stopping either stops both, and Capacitor restores the native config.
19
+ *
20
+ * @param {{ root?: string, platform: string, port?: number, args?: string[], spawn?: typeof nodeSpawn, isReady?: (url: string) => Promise<boolean> }} options
21
+ * `args` go to `cap run`, such as `--target <id>`.
22
+ * @returns {Promise<number>} The exit code of `cap run`.
23
+ */
24
+ export async function dev({
25
+ root = process.cwd(),
26
+ platform,
27
+ port = DEV_PORT,
28
+ args = [],
29
+ spawn = nodeSpawn,
30
+ isReady = respondsAt,
31
+ }) {
32
+ if (!PLATFORMS.includes(platform)) {
33
+ throw new Error(`Usage: inglorious-capacitor dev <ios|android>`)
34
+ }
35
+
36
+ const { webDir } = await readSettings(root, {})
37
+ if (!existsSync(path.join(root, webDir, "index.html"))) {
38
+ await run(spawn, "vite", ["build"], root)
39
+ }
40
+
41
+ const commands = devCommands({ platform, port, args })
42
+ const server = spawn(...commands.server, { cwd: root, stdio: "inherit" })
43
+ try {
44
+ await waitFor(() => isReady(`http://localhost:${port}`))
45
+ const ignoreInterrupt = () => {}
46
+ process.on("SIGINT", ignoreInterrupt)
47
+ try {
48
+ return await run(spawn, ...commands.app, root)
49
+ } finally {
50
+ process.off("SIGINT", ignoreInterrupt)
51
+ }
52
+ } finally {
53
+ server.kill()
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The two processes `dev` starts.
59
+ *
60
+ * @param {{ platform: string, port: number, args: string[] }} options
61
+ */
62
+ export function devCommands({ platform, port, args }) {
63
+ return {
64
+ server: ["vite", ["--host", "--port", String(port), "--strictPort"]],
65
+ app: [
66
+ "cap",
67
+ ["run", platform, "--live-reload", "--port", String(port), ...args],
68
+ ],
69
+ }
70
+ }
71
+
72
+ function run(spawn, command, args, cwd) {
73
+ return new Promise((resolve, reject) => {
74
+ const child = spawn(command, args, { cwd, stdio: "inherit" })
75
+ child.on("error", reject)
76
+ child.on("exit", (code) => {
77
+ if (code)
78
+ reject(new Error(`${command} ${args.join(" ")} exited with ${code}`))
79
+ else resolve(SUCCESS)
80
+ })
81
+ })
82
+ }
83
+
84
+ async function waitFor(check) {
85
+ const deadline = Date.now() + READY_TIMEOUT_MS
86
+ while (!(await check())) {
87
+ if (Date.now() > deadline) {
88
+ throw new Error("The dev server didn't start in time")
89
+ }
90
+ await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS))
91
+ }
92
+ }
93
+
94
+ async function respondsAt(url) {
95
+ try {
96
+ await fetch(url)
97
+ return true
98
+ } catch {
99
+ return false
100
+ }
101
+ }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { assets } from "./assets/index.js"
2
2
  export { readSettings } from "./config.js"
3
3
  export { configure } from "./configure/index.js"
4
+ export { dev } from "./dev/index.js"
4
5
  export { listingAssets } from "./listing-assets/index.js"
package/types/index.d.ts CHANGED
@@ -69,6 +69,8 @@ export interface ProjectSettings {
69
69
  root: string
70
70
  appId: string
71
71
  appName: string
72
+ /** The built web app, from the Capacitor config. */
73
+ webDir: string
72
74
  appLinks: Required<NonNullable<NativeConfig["appLinks"]>> | null
73
75
  ios: {
74
76
  teamId: string | null
@@ -185,3 +187,18 @@ export declare function assets(options?: {
185
187
  root?: string
186
188
  platforms?: ("ios" | "android")[]
187
189
  }): Promise<void>
190
+
191
+ /**
192
+ * Runs the app on a device or emulator with live reload: Vite listens on the
193
+ * local network and `cap run --live-reload` points the app at it. Builds the
194
+ * web app first if it was never built.
195
+ * @returns The exit code of `cap run`.
196
+ */
197
+ export declare function dev(options: {
198
+ root?: string
199
+ platform: "ios" | "android"
200
+ /** Defaults to 5173. */
201
+ port?: number
202
+ /** More options for `cap run`, such as `["--target", "<id>"]`. */
203
+ args?: string[]
204
+ }): Promise<number>