@inglorious/create-app 1.1.1 → 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,10 +61,19 @@ 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
 
70
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:
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,372 +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
-
233
- function copyDir(src, dest) {
234
- fs.mkdirSync(dest, { recursive: true })
235
-
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
192
 
242
- // Rename gitignore to .gitignore
243
- if (entry.name === "gitignore") {
244
- fs.copyFileSync(srcPath, path.join(dest, ".gitignore"))
245
- continue
246
- }
193
+ const pkgPath = path.join(targetDir, "package.json")
194
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
195
+ pkg.name = projectName
247
196
 
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.ts`), // HACK: apparently only ts config works!
320
- createCapacitorConfigSource(projectName, extension),
321
- )
322
-
323
- appendMobileSetupToReadme(targetDir)
324
- }
325
-
326
- function appendMobileSetupToReadme(targetDir) {
327
- const readmePath = path.join(targetDir, "README.md")
328
- if (!fs.existsSync(readmePath)) {
329
- return
330
- }
331
-
332
- const readme = fs.readFileSync(readmePath, "utf-8")
333
- if (readme.includes("## Mobile Setup")) {
334
- return
335
- }
336
-
337
- const mobileReadme = `
338
- ## Mobile Setup
339
-
340
- This project includes Capacitor for Android and iOS support.
341
-
342
- 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.
343
-
344
- ### Android
345
-
346
- Before you run the native apps, make sure you have:
347
-
348
- - Android Studio installed with the Android SDK: https://developer.android.com/studio
349
- - 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.
350
- - The SDK path available via ANDROID_HOME (for example, on macOS this is often $HOME/Library/Android/sdk)
351
-
352
- On macOS, the following shell variables are commonly needed:
353
-
354
- \`\`\`bash
355
- export JAVA_HOME="/path/to/jdk-21"
356
- export ANDROID_HOME="$HOME/Library/Android/sdk"
357
- export PATH="$ANDROID_HOME/platform-tools:$PATH"
358
- \`\`\`
359
-
360
- ### iOS
361
-
362
- For iOS you also need the Apple toolchain and device setup on a Mac:
363
-
364
- - Xcode from the App Store
365
- - iOS development tools and simulator components (this can be very large, often multiple GB)
366
- - A valid Apple Developer team / signing identity in Xcode
367
- - Device trust setup for your iPhone in Settings > General > VPN & Device Management
368
-
369
- The most common first-time setup commands are:
370
-
371
- \`\`\`bash
372
- sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
373
- sudo xcodebuild -license accept
374
- \`\`\`
375
-
376
- 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.
377
-
378
- ### Official resources
379
206
 
380
- - Android: https://capacitorjs.com/docs/android
381
- - iOS: https://capacitorjs.com/docs/ios
382
- - Android Studio: https://developer.android.com/studio
383
- - Xcode: https://developer.apple.com/xcode/
384
-
385
- Then sync and open the native project:
386
-
387
- \`\`\`bash
388
- pnpm mobile:sync
389
- pnpm mobile:android
390
- # or
391
- pnpm mobile:ios
392
- \`\`\`
393
- `
394
-
395
- fs.writeFileSync(
396
- readmePath,
397
- `${readme.trimEnd()}${EOL}${EOL}${mobileReadme.trim()}${EOL}`,
398
- )
399
- }
400
-
401
- function createServiceWorkerSource() {
402
- return `const CACHE_NAME = "inglorious-app-v1"
403
- const APP_SHELL = ["/", "/index.html", "/logo.png", "/style.css"]
404
-
405
- self.addEventListener("install", (event) => {
406
- event.waitUntil(
407
- caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
408
- )
409
- self.skipWaiting()
410
- })
411
-
412
- self.addEventListener("activate", (event) => {
413
- event.waitUntil(
414
- caches
415
- .keys()
416
- .then((keys) =>
417
- Promise.all(keys.map((key) => key === CACHE_NAME ? null : caches.delete(key))),
418
- ),
419
- )
420
- self.clients.claim()
421
- })
422
-
423
- self.addEventListener("fetch", (event) => {
424
- if (event.request.method !== "GET") {
425
- return
426
- }
427
-
428
- event.respondWith(
429
- caches.match(event.request).then((cachedResponse) => {
430
- if (cachedResponse) {
431
- return cachedResponse
432
- }
433
-
434
- return fetch(event.request).catch(() => {
435
- if (event.request.mode === "navigate") {
436
- 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
+ }
437
214
  }
215
+ }
216
+ }
438
217
 
439
- throw new Error("Network request failed")
440
- })
441
- }),
442
- )
443
- })
444
- `
445
- }
446
-
447
- function createWebManifest(projectName) {
448
- return {
449
- name: projectName,
450
- short_name: projectName,
451
- description: "An Inglorious Web app.",
452
- start_url: "/",
453
- display: "standalone",
454
- background_color: "#ffffff",
455
- theme_color: "#111827",
456
- icons: [
457
- {
458
- src: "/logo.png",
459
- sizes: "192x192",
460
- type: "image/png",
461
- },
462
- {
463
- src: "/logo.png",
464
- sizes: "512x512",
465
- type: "image/png",
466
- },
467
- ],
468
- }
469
- }
470
-
471
- function injectPwaHeadTags(indexPath) {
472
- let index = fs.readFileSync(indexPath, "utf-8")
473
-
474
- if (!index.includes('rel="manifest"')) {
475
- index = index.replace(
476
- /^([ \t]*<link rel="icon"[^>]*>\r?\n)/m,
477
- `$1 <link rel="manifest" href="/manifest.webmanifest" />${EOL}`,
478
- )
479
- }
218
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, INDENTATION) + EOL)
480
219
 
481
- if (!index.includes('name="theme-color"')) {
482
- index = index.replace(
483
- /^([ \t]*<meta name="viewport"[^>]*>\r?\n)/m,
484
- `$1 <meta name="theme-color" content="#111827" />${EOL}`,
485
- )
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)
486
228
  }
487
-
488
- fs.writeFileSync(indexPath, index)
489
229
  }
490
230
 
491
- function injectServiceWorkerRegistration(targetDir) {
492
- const mainPath = findMainPath(targetDir)
493
-
494
- if (!mainPath) {
495
- return
496
- }
497
-
498
- let main = fs.readFileSync(mainPath, "utf-8")
499
-
500
- if (!main.includes("@inglorious/web/mobile")) {
501
- main = main.replace(
502
- /^([ \t]*import \{ mount \} from "@inglorious\/web"\r?\n)/m,
503
- (match) =>
504
- `${match}import { isNative } from "@inglorious/web/mobile"${EOL}`,
505
- )
506
- }
507
-
508
- if (!main.includes('navigator.serviceWorker.register("/sw.js")')) {
509
- main = `${main.trimEnd()}${EOL}${EOL}if (!isNative() && "serviceWorker" in navigator) {
510
- navigator.serviceWorker.register("/sw.js")
511
- }${EOL}`
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"
512
241
  }
513
-
514
- fs.writeFileSync(mainPath, main)
515
- }
516
-
517
- function findMainPath(targetDir) {
518
- const candidates = [
519
- path.join(targetDir, "src/main.ts"),
520
- path.join(targetDir, "src/main.js"),
521
- ]
522
-
523
- return candidates.find((candidate) => fs.existsSync(candidate)) ?? null
524
- }
525
-
526
- function createCapacitorConfigSource(projectName /* , extension */) {
527
- const appId = `com.inglorious.${toJavaIdentifier(projectName)}`
528
-
529
- //if (extension === "ts") { // HACK: apparently only ts config files work!
530
- return `import type { CapacitorConfig } from "@capacitor/cli"
531
-
532
- const config: CapacitorConfig = {
533
- appId: "${appId}",
534
- appName: "${projectName}",
535
- webDir: "dist",
536
- }
537
-
538
- export default config
539
- `
540
- }
541
-
542
- function toJavaIdentifier(value) {
543
- const identifier = value
544
- .toLowerCase()
545
- .replace(/[^a-z0-9]+/g, " ")
546
- .trim()
547
- .split(" ")
548
- .filter(Boolean)
549
- .map((part, index) =>
550
- index === FIRST_ITEM_INDEX
551
- ? part
552
- : part.charAt(FIRST_CHARACTER_INDEX).toUpperCase() +
553
- part.slice(AFTER_FIRST_CHARACTER_INDEX),
554
- )
555
- .join("")
556
- .replace(/^[^a-z]+/, "")
557
-
558
- return identifier || "app"
559
242
  }
560
243
 
561
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.1",
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 .",
@@ -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": {
@@ -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": {
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",