@inglorious/create-app 1.1.0 → 1.1.2

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
@@ -31,7 +31,7 @@ npm create @inglorious/app@latest -- --mobile
31
31
  npm create @inglorious/app@latest -- --pwa --mobile
32
32
  ```
33
33
 
34
- - `--pwa` adds a web app manifest, service worker, and browser-only service worker registration.
34
+ - `--pwa` adds `vite-plugin-pwa`, a basic web app manifest configuration, PWA icons, and browser-only service worker registration.
35
35
  - `--mobile` adds Capacitor dependencies, config, and mobile scripts for iOS and Android.
36
36
 
37
37
  ## Templates
@@ -61,12 +61,27 @@ pnpm install
61
61
  pnpm dev
62
62
  ```
63
63
 
64
- When mobile support is enabled, build and sync native projects with:
64
+ When mobile support is enabled, initialize the native platforms once, then build and sync them with:
65
65
 
66
66
  ```bash
67
+ # Run one or both commands once, depending on the platforms you need
68
+ pnpm mobile:init:android
69
+ pnpm mobile:init:ios
70
+
67
71
  pnpm mobile:sync
72
+
73
+ # Build and open the native app
74
+ pnpm mobile:android
75
+ # or
76
+ pnpm mobile:ios
68
77
  ```
69
78
 
79
+ For Android and iOS builds, install Android Studio with the Android SDK and a Java runtime compatible with Capacitor. Capacitor currently works reliably with JDK 21, so prefer JDK 21 over newer Java versions if you hit Java compatibility issues. Set `JAVA_HOME` to your JDK 21 install and `ANDROID_HOME` to your Android SDK path before running the native app. See the official Capacitor setup guides for platform-specific instructions:
80
+
81
+ - Android: https://capacitorjs.com/docs/android
82
+ - iOS: https://capacitorjs.com/docs/ios
83
+ - Android Studio: https://developer.android.com/studio
84
+
70
85
  ## License
71
86
 
72
87
  **MIT License - Free and open source**
package/bin/index.js CHANGED
@@ -8,15 +8,17 @@ import path from "path"
8
8
  import prompts from "prompts"
9
9
  import { fileURLToPath } from "url"
10
10
 
11
- const INDENTATION = 2
12
- const CAPACITOR_VERSION = "^8.5.0"
13
- const AFTER_FIRST_CHARACTER_INDEX = 1
14
- const EMPTY_LENGTH = 0
15
- const FIRST_CHARACTER_INDEX = 0
16
- const FIRST_ITEM_INDEX = 0
17
- const NPM_VIEW_TIMEOUT_MS = 10_000
18
- const USER_ARG_START_INDEX = 2
19
- const VITE_APP_TEMPLATES = new Set(["js", "ts"])
11
+ import { parseCliOptions } from "./lib/cli.js"
12
+ import {
13
+ EMPTY_LENGTH,
14
+ INDENTATION,
15
+ NPM_VIEW_TIMEOUT_MS,
16
+ USER_ARG_START_INDEX,
17
+ VITE_APP_TEMPLATES,
18
+ } from "./lib/constants.js"
19
+ import { copyDir } from "./lib/file.js"
20
+ import { addMobileSupport } from "./lib/mobile.js"
21
+ import { addPwaSupport } from "./lib/pwa.js"
20
22
 
21
23
  async function main() {
22
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -83,7 +85,6 @@ async function main() {
83
85
  let pwa = cliOptions.pwa === true
84
86
  let mobile = cliOptions.mobile === true
85
87
 
86
- // Ask about renderer for non-minimal templates
87
88
  if (baseTemplate !== "minimal") {
88
89
  const { selectedRenderer } = await prompts(
89
90
  {
@@ -166,18 +167,14 @@ async function main() {
166
167
  try {
167
168
  const targetDir = path.join(process.cwd(), projectName)
168
169
 
169
- // Determine the actual template directory based on base template and renderer
170
170
  let templateName = baseTemplate
171
171
  if (baseTemplate !== "minimal" && renderer !== "lit-html") {
172
172
  templateName = `${baseTemplate}-${renderer}`
173
173
  }
174
174
 
175
175
  const templateDir = path.join(templatesRoot, templateName)
176
-
177
- // Create project directory and copy template files
178
176
  copyDir(templateDir, targetDir)
179
177
 
180
- // Update README.md with project name
181
178
  const readmePath = path.join(targetDir, "README.md")
182
179
  if (fs.existsSync(readmePath)) {
183
180
  let readme = fs.readFileSync(readmePath, "utf-8")
@@ -190,306 +187,58 @@ async function main() {
190
187
  console.log(`\nNext steps:`)
191
188
  console.log(` cd ${projectName}`)
192
189
  console.log(` Open index.html in your browser`)
193
- } else {
194
- // Update package.json with project name
195
- const pkgPath = path.join(targetDir, "package.json")
196
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
197
- pkg.name = projectName
198
-
199
- if (pwa) {
200
- spinner.text = "Adding PWA support..."
201
- addPwaSupport(targetDir, projectName)
202
- }
203
-
204
- if (mobile) {
205
- spinner.text = "Adding Capacitor support..."
206
- addMobileSupport(targetDir, pkg, projectName, templateName)
207
- }
208
-
209
- for (const depType of ["dependencies", "devDependencies"]) {
210
- if (pkg[depType]) {
211
- for (const [name, version] of Object.entries(pkg[depType])) {
212
- if (version.startsWith("workspace:")) {
213
- spinner.text = `Fetching latest version of ${name}...`
214
- pkg[depType][name] = getLatestVersion(name)
215
- }
216
- }
217
- }
218
- }
219
-
220
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, INDENTATION) + EOL)
221
-
222
- spinner.succeed(`Created ${projectName} at ${targetDir}`)
223
- console.log(`\nNext steps:`)
224
- console.log(` cd ${projectName}`)
225
- console.log(` pnpm install`)
226
- console.log(` pnpm dev`)
190
+ return
227
191
  }
228
- } catch (error) {
229
- spinner.fail("Operation failed.")
230
- console.error(error)
231
- }
232
192
 
233
- function copyDir(src, dest) {
234
- fs.mkdirSync(dest, { recursive: true })
193
+ const pkgPath = path.join(targetDir, "package.json")
194
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
195
+ pkg.name = projectName
235
196
 
236
- const entries = fs.readdirSync(src, { withFileTypes: true })
237
-
238
- for (const entry of entries) {
239
- const srcPath = path.join(src, entry.name)
240
- const destPath = path.join(dest, entry.name)
241
-
242
- // Rename gitignore to .gitignore
243
- if (entry.name === "gitignore") {
244
- fs.copyFileSync(srcPath, path.join(dest, ".gitignore"))
245
- continue
246
- }
247
-
248
- if (entry.isDirectory()) {
249
- copyDir(srcPath, destPath)
250
- } else {
251
- fs.copyFileSync(srcPath, destPath)
252
- }
197
+ if (pwa) {
198
+ spinner.text = "Adding PWA support..."
199
+ addPwaSupport(targetDir, pkg, projectName)
253
200
  }
254
- }
255
201
 
256
- function getLatestVersion(packageName) {
257
- try {
258
- const version = execSync(`npm view ${packageName} version`, {
259
- encoding: "utf-8",
260
- stdio: "pipe", // Prevent npm view output from cluttering the console
261
- timeout: NPM_VIEW_TIMEOUT_MS,
262
- }).trim()
263
- return `^${version}`
264
- } catch {
265
- return "latest" // fallback
202
+ if (mobile) {
203
+ spinner.text = "Adding Capacitor support..."
204
+ addMobileSupport(targetDir, pkg, projectName)
266
205
  }
267
- }
268
- }
269
-
270
- function parseCliOptions(args) {
271
- return {
272
- mobile: readBooleanFlag(args, "mobile"),
273
- pwa: readBooleanFlag(args, "pwa"),
274
- }
275
- }
276
-
277
- function readBooleanFlag(args, name) {
278
- if (args.includes(`--${name}`)) {
279
- return true
280
- }
281
-
282
- if (args.includes(`--no-${name}`)) {
283
- return false
284
- }
285
-
286
- return null
287
- }
288
-
289
- function addPwaSupport(targetDir, projectName) {
290
- const publicDir = path.join(targetDir, "public")
291
- fs.mkdirSync(publicDir, { recursive: true })
292
- fs.writeFileSync(path.join(publicDir, "sw.js"), createServiceWorkerSource())
293
- fs.writeFileSync(
294
- path.join(publicDir, "manifest.webmanifest"),
295
- JSON.stringify(createWebManifest(projectName), null, INDENTATION) + EOL,
296
- )
297
-
298
- injectPwaHeadTags(path.join(targetDir, "index.html"))
299
- injectServiceWorkerRegistration(targetDir)
300
- }
301
-
302
- function addMobileSupport(targetDir, pkg, projectName, templateName) {
303
- pkg.dependencies ??= {}
304
- pkg.devDependencies ??= {}
305
- pkg.scripts ??= {}
306
-
307
- pkg.dependencies["@capacitor/core"] = CAPACITOR_VERSION
308
- pkg.devDependencies["@capacitor/android"] = CAPACITOR_VERSION
309
- pkg.devDependencies["@capacitor/cli"] = CAPACITOR_VERSION
310
- pkg.devDependencies["@capacitor/ios"] = CAPACITOR_VERSION
311
-
312
- pkg.scripts.cap = "cap"
313
- pkg.scripts["mobile:sync"] = "pnpm build && cap sync"
314
- pkg.scripts["mobile:ios"] = "pnpm build && cap run ios"
315
- pkg.scripts["mobile:android"] = "pnpm build && cap run android"
316
-
317
- const extension = templateName.startsWith("ts") ? "ts" : "js"
318
- fs.writeFileSync(
319
- path.join(targetDir, `capacitor.config.${extension}`),
320
- createCapacitorConfigSource(projectName, extension),
321
- )
322
- }
323
-
324
- function createServiceWorkerSource() {
325
- return `const CACHE_NAME = "inglorious-app-v1"
326
- const APP_SHELL = ["/", "/index.html", "/logo.png", "/style.css"]
327
-
328
- self.addEventListener("install", (event) => {
329
- event.waitUntil(
330
- caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
331
- )
332
- self.skipWaiting()
333
- })
334
-
335
- self.addEventListener("activate", (event) => {
336
- event.waitUntil(
337
- caches
338
- .keys()
339
- .then((keys) =>
340
- Promise.all(keys.map((key) => key === CACHE_NAME ? null : caches.delete(key))),
341
- ),
342
- )
343
- self.clients.claim()
344
- })
345
-
346
- self.addEventListener("fetch", (event) => {
347
- if (event.request.method !== "GET") {
348
- return
349
- }
350
206
 
351
- event.respondWith(
352
- caches.match(event.request).then((cachedResponse) => {
353
- if (cachedResponse) {
354
- return cachedResponse
355
- }
356
-
357
- return fetch(event.request).catch(() => {
358
- if (event.request.mode === "navigate") {
359
- return caches.match("/index.html")
207
+ for (const depType of ["dependencies", "devDependencies"]) {
208
+ if (pkg[depType]) {
209
+ for (const [name, version] of Object.entries(pkg[depType])) {
210
+ if (version.startsWith("workspace:")) {
211
+ spinner.text = `Fetching latest version of ${name}...`
212
+ pkg[depType][name] = getLatestVersion(name)
213
+ }
360
214
  }
215
+ }
216
+ }
361
217
 
362
- throw new Error("Network request failed")
363
- })
364
- }),
365
- )
366
- })
367
- `
368
- }
369
-
370
- function createWebManifest(projectName) {
371
- return {
372
- name: projectName,
373
- short_name: projectName,
374
- description: "An Inglorious Web app.",
375
- start_url: "/",
376
- display: "standalone",
377
- background_color: "#ffffff",
378
- theme_color: "#111827",
379
- icons: [
380
- {
381
- src: "/logo.png",
382
- sizes: "192x192",
383
- type: "image/png",
384
- },
385
- {
386
- src: "/logo.png",
387
- sizes: "512x512",
388
- type: "image/png",
389
- },
390
- ],
391
- }
392
- }
393
-
394
- function injectPwaHeadTags(indexPath) {
395
- let index = fs.readFileSync(indexPath, "utf-8")
396
-
397
- if (!index.includes('rel="manifest"')) {
398
- index = index.replace(
399
- /^([ \t]*<link rel="icon"[^>]*>\r?\n)/m,
400
- `$1 <link rel="manifest" href="/manifest.webmanifest" />${EOL}`,
401
- )
402
- }
403
-
404
- if (!index.includes('name="theme-color"')) {
405
- index = index.replace(
406
- /^([ \t]*<meta name="viewport"[^>]*>\r?\n)/m,
407
- `$1 <meta name="theme-color" content="#111827" />${EOL}`,
408
- )
409
- }
410
-
411
- fs.writeFileSync(indexPath, index)
412
- }
413
-
414
- function injectServiceWorkerRegistration(targetDir) {
415
- const mainPath = findMainPath(targetDir)
416
-
417
- if (!mainPath) {
418
- return
419
- }
420
-
421
- let main = fs.readFileSync(mainPath, "utf-8")
422
-
423
- if (!main.includes("@inglorious/web/mobile")) {
424
- main = main.replace(
425
- /^([ \t]*import \{ mount \} from "@inglorious\/web"\r?\n)/m,
426
- (match) =>
427
- `${match}import { isNative } from "@inglorious/web/mobile"${EOL}`,
428
- )
429
- }
218
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, INDENTATION) + EOL)
430
219
 
431
- if (!main.includes('navigator.serviceWorker.register("/sw.js")')) {
432
- main = `${main.trimEnd()}${EOL}${EOL}if (!isNative() && "serviceWorker" in navigator) {
433
- navigator.serviceWorker.register("/sw.js")
434
- }${EOL}`
220
+ spinner.succeed(`Created ${projectName} at ${targetDir}`)
221
+ console.log(`\nNext steps:`)
222
+ console.log(` cd ${projectName}`)
223
+ console.log(` pnpm install`)
224
+ console.log(` pnpm dev`)
225
+ } catch (error) {
226
+ spinner.fail("Operation failed.")
227
+ console.error(error)
435
228
  }
436
-
437
- fs.writeFileSync(mainPath, main)
438
- }
439
-
440
- function findMainPath(targetDir) {
441
- const candidates = [
442
- path.join(targetDir, "src/main.ts"),
443
- path.join(targetDir, "src/main.js"),
444
- ]
445
-
446
- return candidates.find((candidate) => fs.existsSync(candidate)) ?? null
447
229
  }
448
230
 
449
- function createCapacitorConfigSource(projectName, extension) {
450
- const appId = `com.inglorious.${toJavaIdentifier(projectName)}`
451
-
452
- if (extension === "ts") {
453
- return `import type { CapacitorConfig } from "@capacitor/cli"
454
-
455
- const config: CapacitorConfig = {
456
- appId: "${appId}",
457
- appName: "${projectName}",
458
- webDir: "dist",
459
- }
460
-
461
- export default config
462
- `
231
+ function getLatestVersion(packageName) {
232
+ try {
233
+ const version = execSync(`npm view ${packageName} version`, {
234
+ encoding: "utf-8",
235
+ stdio: "pipe",
236
+ timeout: NPM_VIEW_TIMEOUT_MS,
237
+ }).trim()
238
+ return `^${version}`
239
+ } catch {
240
+ return "latest"
463
241
  }
464
-
465
- return `/** @type {import("@capacitor/cli").CapacitorConfig} */
466
- const config = {
467
- appId: "${appId}",
468
- appName: "${projectName}",
469
- webDir: "dist",
470
- }
471
-
472
- export default config
473
- `
474
- }
475
-
476
- function toJavaIdentifier(value) {
477
- const identifier = value
478
- .toLowerCase()
479
- .replace(/[^a-z0-9]+/g, " ")
480
- .trim()
481
- .split(" ")
482
- .filter(Boolean)
483
- .map((part, index) =>
484
- index === FIRST_ITEM_INDEX
485
- ? part
486
- : part.charAt(FIRST_CHARACTER_INDEX).toUpperCase() +
487
- part.slice(AFTER_FIRST_CHARACTER_INDEX),
488
- )
489
- .join("")
490
- .replace(/^[^a-z]+/, "")
491
-
492
- return identifier || "app"
493
242
  }
494
243
 
495
244
  main().catch((e) => {
package/bin/lib/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ export function parseCliOptions(args) {
2
+ return {
3
+ mobile: readBooleanFlag(args, "mobile"),
4
+ pwa: readBooleanFlag(args, "pwa"),
5
+ }
6
+ }
7
+
8
+ export function readBooleanFlag(args, name) {
9
+ if (args.includes(`--${name}`)) {
10
+ return true
11
+ }
12
+
13
+ if (args.includes(`--no-${name}`)) {
14
+ return false
15
+ }
16
+
17
+ return null
18
+ }
@@ -0,0 +1,14 @@
1
+ export const CAPACITOR_VERSION = "^8.5.1"
2
+ export const VITE_PLUGIN_PWA_VERSION = "^1.3.0"
3
+ export const VITE_PWA_ASSETS_GENERATOR_VERSION = "^1.0.2"
4
+ export const TYPESCRIPT_VERSION = "^7.0.2"
5
+ export const WORKBOX_WINDOW_VERSION = "^7.4.1"
6
+
7
+ export const INDENTATION = 2
8
+ export const AFTER_FIRST_CHARACTER_INDEX = 1
9
+ export const EMPTY_LENGTH = 0
10
+ export const FIRST_CHARACTER_INDEX = 0
11
+ export const FIRST_ITEM_INDEX = 0
12
+ export const NPM_VIEW_TIMEOUT_MS = 10_000
13
+ export const USER_ARG_START_INDEX = 2
14
+ export const VITE_APP_TEMPLATES = new Set(["js", "ts"])
@@ -0,0 +1,88 @@
1
+ import fs from "fs"
2
+ import { EOL } from "os"
3
+ import path from "path"
4
+
5
+ import {
6
+ AFTER_FIRST_CHARACTER_INDEX,
7
+ FIRST_CHARACTER_INDEX,
8
+ FIRST_ITEM_INDEX,
9
+ } from "./constants.js"
10
+
11
+ export function copyDir(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true })
13
+
14
+ const entries = fs.readdirSync(src, { withFileTypes: true })
15
+
16
+ for (const entry of entries) {
17
+ const srcPath = path.join(src, entry.name)
18
+ const destPath = path.join(dest, entry.name)
19
+
20
+ if (entry.name === "gitignore") {
21
+ fs.copyFileSync(srcPath, path.join(dest, ".gitignore"))
22
+ continue
23
+ }
24
+
25
+ if (entry.isDirectory()) {
26
+ copyDir(srcPath, destPath)
27
+ } else {
28
+ fs.copyFileSync(srcPath, destPath)
29
+ }
30
+ }
31
+ }
32
+
33
+ export function findMainPath(targetDir) {
34
+ const candidates = [
35
+ path.join(targetDir, "src/main.ts"),
36
+ path.join(targetDir, "src/main.js"),
37
+ ]
38
+
39
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null
40
+ }
41
+
42
+ export function findViteConfigPath(targetDir) {
43
+ const candidates = [
44
+ path.join(targetDir, "vite.config.ts"),
45
+ path.join(targetDir, "vite.config.js"),
46
+ ]
47
+
48
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null
49
+ }
50
+
51
+ export function injectAfterImport(source, importSource, newImport) {
52
+ const importLinePattern = new RegExp(
53
+ `^([ \\t]*import .+ from "${escapeRegExp(importSource)}"\\r?\\n)`,
54
+ "m",
55
+ )
56
+
57
+ if (!importLinePattern.test(source)) {
58
+ return `${newImport}${EOL}${source}`
59
+ }
60
+
61
+ return source.replace(
62
+ importLinePattern,
63
+ (match) => `${match}${newImport}${EOL}`,
64
+ )
65
+ }
66
+
67
+ export function escapeRegExp(value) {
68
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
69
+ }
70
+
71
+ export function toJavaIdentifier(value) {
72
+ const identifier = value
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9]+/g, " ")
75
+ .trim()
76
+ .split(" ")
77
+ .filter(Boolean)
78
+ .map((part, index) =>
79
+ index === FIRST_ITEM_INDEX
80
+ ? part
81
+ : part.charAt(FIRST_CHARACTER_INDEX).toUpperCase() +
82
+ part.slice(AFTER_FIRST_CHARACTER_INDEX),
83
+ )
84
+ .join("")
85
+ .replace(/^[^a-z]+/, "")
86
+
87
+ return identifier || "app"
88
+ }
@@ -0,0 +1,122 @@
1
+ import fs from "fs"
2
+ import { EOL } from "os"
3
+ import path from "path"
4
+
5
+ import { CAPACITOR_VERSION, TYPESCRIPT_VERSION } from "./constants.js"
6
+ import { toJavaIdentifier } from "./file.js"
7
+
8
+ export function addMobileSupport(targetDir, pkg, projectName) {
9
+ pkg.dependencies ??= {}
10
+ pkg.devDependencies ??= {}
11
+ pkg.scripts ??= {}
12
+
13
+ pkg.dependencies["@capacitor/core"] = CAPACITOR_VERSION
14
+ pkg.devDependencies["@capacitor/android"] = CAPACITOR_VERSION
15
+ pkg.devDependencies["@capacitor/cli"] = CAPACITOR_VERSION
16
+ pkg.devDependencies["@capacitor/ios"] = CAPACITOR_VERSION
17
+ pkg.devDependencies["typescript"] = TYPESCRIPT_VERSION
18
+
19
+ pkg.scripts.cap = "cap"
20
+ pkg.scripts["mobile:init:ios"] = "cap add ios"
21
+ pkg.scripts["mobile:init:android"] = "cap add android"
22
+ pkg.scripts["mobile:sync"] = "pnpm build && cap sync"
23
+ pkg.scripts["mobile:ios"] = "pnpm build && cap run ios"
24
+ pkg.scripts["mobile:android"] = "pnpm build && cap run android"
25
+
26
+ fs.writeFileSync(
27
+ path.join(targetDir, "capacitor.config.ts"),
28
+ createCapacitorConfigSource(projectName),
29
+ )
30
+
31
+ appendMobileSetupToReadme(targetDir)
32
+ }
33
+
34
+ export function appendMobileSetupToReadme(targetDir) {
35
+ const readmePath = path.join(targetDir, "README.md")
36
+ if (!fs.existsSync(readmePath)) {
37
+ return
38
+ }
39
+
40
+ const readme = fs.readFileSync(readmePath, "utf-8")
41
+ if (readme.includes("## Mobile Setup")) {
42
+ return
43
+ }
44
+
45
+ const mobileReadme = `
46
+ ## Mobile Setup
47
+
48
+ This project includes Capacitor for Android and iOS support.
49
+
50
+ Please note that mobile development depends on external platform tooling that is not installed or managed by the CLI. If the app does not just work after scaffolding, it is usually because one or more native prerequisites are missing on the machine.
51
+
52
+ ### Android
53
+
54
+ Before you run the native apps, make sure you have:
55
+
56
+ - Android Studio installed with the Android SDK: https://developer.android.com/studio
57
+ - A Java Development Kit compatible with Capacitor. Capacitor is currently known to work reliably with JDK 21, so prefer JDK 21 over newer versions if you run into Java compatibility errors.
58
+ - The SDK path available via ANDROID_HOME (for example, on macOS this is often $HOME/Library/Android/sdk)
59
+
60
+ On macOS, the following shell variables are commonly needed:
61
+
62
+ \`\`\`bash
63
+ export JAVA_HOME="/path/to/jdk-21"
64
+ export ANDROID_HOME="$HOME/Library/Android/sdk"
65
+ export PATH="$ANDROID_HOME/platform-tools:$PATH"
66
+ \`\`\`
67
+
68
+ ### iOS
69
+
70
+ For iOS you also need the Apple toolchain and device setup on a Mac:
71
+
72
+ - Xcode from the App Store
73
+ - iOS development tools and simulator components (this can be very large, often multiple GB)
74
+ - A valid Apple Developer team / signing identity in Xcode
75
+ - Device trust setup for your iPhone in Settings > General > VPN & Device Management
76
+
77
+ The most common first-time setup commands are:
78
+
79
+ \`\`\`bash
80
+ sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
81
+ sudo xcodebuild -license accept
82
+ \`\`\`
83
+
84
+ If you are using a physical device, you may also need to approve the developer certificate on the device and ensure the app is signed with a valid team.
85
+
86
+ ### Official resources
87
+
88
+ - Android: https://capacitorjs.com/docs/android
89
+ - iOS: https://capacitorjs.com/docs/ios
90
+ - Android Studio: https://developer.android.com/studio
91
+ - Xcode: https://developer.apple.com/xcode/
92
+
93
+ Then sync and open the native project:
94
+
95
+ \`\`\`bash
96
+ pnpm mobile:sync
97
+ pnpm mobile:android
98
+ # or
99
+ pnpm mobile:ios
100
+ \`\`\`
101
+ `
102
+
103
+ fs.writeFileSync(
104
+ readmePath,
105
+ `${readme.trimEnd()}${EOL}${EOL}${mobileReadme.trim()}${EOL}`,
106
+ )
107
+ }
108
+
109
+ export function createCapacitorConfigSource(projectName) {
110
+ const appId = `com.inglorious.${toJavaIdentifier(projectName)}`
111
+
112
+ return `import type { CapacitorConfig } from "@capacitor/cli"
113
+
114
+ const config: CapacitorConfig = {
115
+ appId: "${appId}",
116
+ appName: "${projectName}",
117
+ webDir: "dist",
118
+ }
119
+
120
+ export default config
121
+ `
122
+ }
package/bin/lib/pwa.js ADDED
@@ -0,0 +1,122 @@
1
+ import fs from "fs"
2
+ import { EOL } from "os"
3
+ import path from "path"
4
+
5
+ import {
6
+ VITE_PLUGIN_PWA_VERSION,
7
+ VITE_PWA_ASSETS_GENERATOR_VERSION,
8
+ WORKBOX_WINDOW_VERSION,
9
+ } from "./constants.js"
10
+ import { findMainPath, findViteConfigPath, injectAfterImport } from "./file.js"
11
+
12
+ export function addPwaSupport(targetDir, pkg, projectName) {
13
+ pkg.devDependencies ??= {}
14
+ pkg.devDependencies["vite-plugin-pwa"] = VITE_PLUGIN_PWA_VERSION
15
+ pkg.devDependencies["@vite-pwa/assets-generator"] =
16
+ VITE_PWA_ASSETS_GENERATOR_VERSION
17
+ pkg.devDependencies["workbox-window"] = WORKBOX_WINDOW_VERSION
18
+
19
+ const publicDir = path.join(targetDir, "public")
20
+ fs.mkdirSync(publicDir, { recursive: true })
21
+
22
+ injectPwaPluginConfig(targetDir, projectName)
23
+ writePwaRegistration(targetDir)
24
+ injectServiceWorkerRegistration(targetDir)
25
+ }
26
+
27
+ export function injectServiceWorkerRegistration(targetDir) {
28
+ const mainPath = findMainPath(targetDir)
29
+
30
+ if (!mainPath) {
31
+ return
32
+ }
33
+
34
+ let main = fs.readFileSync(mainPath, "utf-8")
35
+
36
+ if (!main.includes("./pwa")) {
37
+ main = injectAfterImport(main, "@inglorious/web", `import "./pwa"`)
38
+ }
39
+
40
+ fs.writeFileSync(mainPath, main)
41
+ }
42
+
43
+ export function injectPwaPluginConfig(targetDir, projectName) {
44
+ const viteConfigPath = findViteConfigPath(targetDir)
45
+
46
+ if (!viteConfigPath) {
47
+ return
48
+ }
49
+
50
+ let viteConfig = fs.readFileSync(viteConfigPath, "utf-8")
51
+
52
+ if (!viteConfig.includes("vite-plugin-pwa")) {
53
+ viteConfig = injectAfterImport(
54
+ viteConfig,
55
+ "vite",
56
+ `import { VitePWA } from "vite-plugin-pwa"`,
57
+ )
58
+ }
59
+
60
+ if (!viteConfig.includes("VitePWA(")) {
61
+ viteConfig = viteConfig.replace(
62
+ /plugins: \[/,
63
+ `plugins: [${EOL} ${createVitePwaConfig(projectName)},`,
64
+ )
65
+ }
66
+
67
+ fs.writeFileSync(viteConfigPath, viteConfig)
68
+ }
69
+
70
+ export function writePwaRegistration(targetDir) {
71
+ const mainPath = findMainPath(targetDir)
72
+
73
+ if (!mainPath) {
74
+ return
75
+ }
76
+
77
+ const extension = path.extname(mainPath)
78
+ const pwaPath = path.join(targetDir, `src/pwa${extension}`)
79
+
80
+ fs.writeFileSync(pwaPath, createPwaRegistrationSource(extension))
81
+ }
82
+
83
+ export function createPwaRegistrationSource(extension) {
84
+ if (extension === ".ts") {
85
+ return `/// <reference types="vite-plugin-pwa/client" />
86
+ import { isNative } from "@inglorious/web/mobile"
87
+ import { registerSW } from "virtual:pwa-register"
88
+
89
+ if (!isNative()) {
90
+ registerSW()
91
+ }
92
+ `
93
+ }
94
+
95
+ return `import { isNative } from "@inglorious/web/mobile"
96
+ import { registerSW } from "virtual:pwa-register"
97
+
98
+ if (!isNative()) {
99
+ registerSW()
100
+ }
101
+ `
102
+ }
103
+
104
+ export function createVitePwaConfig(projectName) {
105
+ return `VitePWA({
106
+ injectRegister: false,
107
+ registerType: "autoUpdate",
108
+ pwaAssets: {
109
+ image: "public/logo.png",
110
+ preset: "minimal-2023",
111
+ },
112
+ manifest: {
113
+ name: "${projectName}",
114
+ short_name: "${projectName}",
115
+ description: "An Inglorious Web app.",
116
+ theme_color: "#ffffff",
117
+ background_color: "#ffffff",
118
+ display: "standalone",
119
+ start_url: "/",
120
+ },
121
+ })`
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/create-app",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A scaffolding tool to quickly create a new app with Inglorious Web.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -37,8 +37,8 @@
37
37
  "prompts": "^2.4.2"
38
38
  },
39
39
  "devDependencies": {
40
- "prettier": "^3.6.2",
41
- "@inglorious/eslint-config": "1.1.2"
40
+ "@inglorious/eslint-config": "1.1.2",
41
+ "prettier": "^3.6.2"
42
42
  },
43
43
  "scripts": {
44
44
  "format": "prettier --write .",
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -19,13 +19,14 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
- "eslint": "^9.39.1",
23
- "prettier": "^3.6.2",
24
- "rollup-plugin-minify-template-literals": "^1.1.7",
25
- "sharp": "^0.34.5",
26
- "svgo": "^4.0.0",
27
- "vite-plugin-image-optimizer": "^2.0.3",
28
- "vite": "^7.1.6"
22
+ "@inglorious/vite-plugin-hmr": "workspace:*",
23
+ "eslint": "^10.9.1",
24
+ "prettier": "^3.9.6",
25
+ "rollup-plugin-minify-template-literals": "^2.2.0",
26
+ "sharp": "^0.35.4",
27
+ "svgo": "^4.1.0",
28
+ "vite": "^8.2.2",
29
+ "vite-plugin-image-optimizer": "^2.0.3"
29
30
  },
30
31
  "packageManager": "pnpm@10.15.0",
31
32
  "engines": {
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -19,14 +19,15 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
+ "@inglorious/vite-plugin-hmr": "workspace:*",
22
23
  "@inglorious/vite-plugin-jsx": "workspace:*",
23
- "eslint": "^9.39.1",
24
- "prettier": "^3.6.2",
25
- "rollup-plugin-minify-template-literals": "^1.1.7",
26
- "sharp": "^0.34.5",
27
- "svgo": "^4.0.0",
28
- "vite-plugin-image-optimizer": "^2.0.3",
29
- "vite": "^7.1.6"
24
+ "eslint": "^10.9.1",
25
+ "prettier": "^3.9.6",
26
+ "rollup-plugin-minify-template-literals": "^2.2.0",
27
+ "sharp": "^0.35.4",
28
+ "svgo": "^4.1.0",
29
+ "vite": "^8.2.2",
30
+ "vite-plugin-image-optimizer": "^2.0.3"
30
31
  },
31
32
  "packageManager": "pnpm@10.15.0",
32
33
  "engines": {
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -19,13 +19,14 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
+ "@inglorious/vite-plugin-hmr": "workspace:*",
22
23
  "@inglorious/vite-plugin-vue": "workspace:*",
23
- "eslint": "^9.39.1",
24
- "prettier": "^3.6.2",
25
- "rollup-plugin-minify-template-literals": "^1.1.7",
26
- "sharp": "^0.34.5",
27
- "svgo": "^4.0.0",
28
- "vite": "^7.1.6",
24
+ "eslint": "^10.9.1",
25
+ "prettier": "^3.9.6",
26
+ "rollup-plugin-minify-template-literals": "^2.2.0",
27
+ "sharp": "^0.35.4",
28
+ "svgo": "^4.1.0",
29
+ "vite": "^8.2.2",
29
30
  "vite-plugin-image-optimizer": "^2.0.3"
30
31
  },
31
32
  "packageManager": "pnpm@10.15.0",
@@ -4,7 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/public/logo.png" />
6
6
  <link rel="stylesheet" href="/public/style.css" />
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta
8
+ name="viewport"
9
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
10
+ />
8
11
  <title>Inglorious App</title>
9
12
  </head>
10
13
 
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@inglorious/eslint-config": "workspace:*",
28
- "eslint": "^9.39.1",
29
- "prettier": "^3.6.2",
28
+ "eslint": "^10.9.1",
29
+ "prettier": "^3.9.6",
30
30
  "serve": "^14.2.1"
31
31
  },
32
32
  "packageManager": "pnpm@10.15.0",
@@ -3,6 +3,7 @@ export default {
3
3
  title: "My SSX App",
4
4
  meta: {
5
5
  description: "Built with @inglorious/ssx",
6
+ viewport: "width=device-width, initial-scale=1.0, viewport-fit=cover",
6
7
  },
7
8
  // SSX includes image optimization by default
8
9
  vite: {
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@inglorious/eslint-config": "workspace:*",
28
- "eslint": "^9.39.1",
29
- "prettier": "^3.6.2",
28
+ "eslint": "^10.9.1",
29
+ "prettier": "^3.9.6",
30
30
  "serve": "^14.2.1"
31
31
  },
32
32
  "packageManager": "pnpm@10.15.0",
@@ -4,6 +4,7 @@ const siteConfig: SiteConfig = {
4
4
  title: "My SSX App",
5
5
  meta: {
6
6
  description: "Built with @inglorious/ssx",
7
+ viewport: "width=device-width, initial-scale=1.0, viewport-fit=cover",
7
8
  },
8
9
  // SSX includes image optimization by default
9
10
  vite: {
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -19,14 +19,15 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
+ "@inglorious/vite-plugin-hmr": "workspace:*",
22
23
  "@types/node": "^25.0.9",
23
- "eslint": "^9.39.1",
24
- "prettier": "^3.6.2",
25
- "rollup-plugin-minify-template-literals": "^1.1.7",
26
- "sharp": "^0.34.5",
27
- "svgo": "^4.0.0",
24
+ "eslint": "^10.9.1",
25
+ "prettier": "^3.9.6",
26
+ "rollup-plugin-minify-template-literals": "^2.2.0",
27
+ "sharp": "^0.35.4",
28
+ "svgo": "^4.1.0",
28
29
  "typescript": "^5.9.2",
29
- "vite": "^7.1.6",
30
+ "vite": "^8.2.2",
30
31
  "vite-plugin-image-optimizer": "^2.0.3"
31
32
  },
32
33
  "packageManager": "pnpm@10.15.0",
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -20,15 +20,16 @@
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
22
  "@inglorious/jsx": "workspace:*",
23
+ "@inglorious/vite-plugin-hmr": "workspace:*",
23
24
  "@inglorious/vite-plugin-jsx": "workspace:*",
24
25
  "@types/node": "^25.0.9",
25
- "eslint": "^9.39.1",
26
- "prettier": "^3.6.2",
27
- "rollup-plugin-minify-template-literals": "^1.1.7",
28
- "sharp": "^0.34.5",
29
- "svgo": "^4.0.0",
26
+ "eslint": "^10.9.1",
27
+ "prettier": "^3.9.6",
28
+ "rollup-plugin-minify-template-literals": "^2.2.0",
29
+ "sharp": "^0.35.4",
30
+ "svgo": "^4.1.0",
30
31
  "typescript": "^5.9.2",
31
- "vite": "^7.1.6",
32
+ "vite": "^8.2.2",
32
33
  "vite-plugin-image-optimizer": "^2.0.3"
33
34
  },
34
35
  "packageManager": "pnpm@10.15.0",
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <link rel="icon" type="image/svg+xml" href="/logo.png" />
7
10
  <link rel="stylesheet" href="/style.css" />
8
11
  <title>Inglorious App</title>
@@ -19,15 +19,16 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@inglorious/eslint-config": "workspace:*",
22
+ "@inglorious/vite-plugin-hmr": "workspace:*",
22
23
  "@inglorious/vite-plugin-vue": "workspace:*",
23
24
  "@types/node": "^25.0.9",
24
- "eslint": "^9.39.1",
25
- "prettier": "^3.6.2",
26
- "rollup-plugin-minify-template-literals": "^1.1.7",
27
- "sharp": "^0.34.5",
28
- "svgo": "^4.0.0",
25
+ "eslint": "^10.9.1",
26
+ "prettier": "^3.9.6",
27
+ "rollup-plugin-minify-template-literals": "^2.2.0",
28
+ "sharp": "^0.35.4",
29
+ "svgo": "^4.1.0",
29
30
  "typescript": "^5.9.2",
30
- "vite": "^7.1.6",
31
+ "vite": "^8.2.2",
31
32
  "vite-plugin-image-optimizer": "^2.0.3"
32
33
  },
33
34
  "packageManager": "pnpm@10.15.0",