@inglorious/create-app 1.0.2 → 1.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 CHANGED
@@ -23,6 +23,17 @@ pnpm create @inglorious/app
23
23
 
24
24
  The CLI will guide you through the setup process, asking for a project name and which template you'd like to use.
25
25
 
26
+ For JavaScript and TypeScript Vite app templates, the CLI can also add optional app targets:
27
+
28
+ ```bash
29
+ npm create @inglorious/app@latest -- --pwa
30
+ npm create @inglorious/app@latest -- --mobile
31
+ npm create @inglorious/app@latest -- --pwa --mobile
32
+ ```
33
+
34
+ - `--pwa` adds a web app manifest, service worker, and browser-only service worker registration.
35
+ - `--mobile` adds Capacitor dependencies, config, and mobile scripts for iOS and Android.
36
+
26
37
  ## Templates
27
38
 
28
39
  The following templates are available to get you started:
@@ -50,6 +61,12 @@ pnpm install
50
61
  pnpm dev
51
62
  ```
52
63
 
64
+ When mobile support is enabled, build and sync native projects with:
65
+
66
+ ```bash
67
+ pnpm mobile:sync
68
+ ```
69
+
53
70
  ## License
54
71
 
55
72
  **MIT License - Free and open source**
package/bin/index.js CHANGED
@@ -9,10 +9,19 @@ import prompts from "prompts"
9
9
  import { fileURLToPath } from "url"
10
10
 
11
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"])
12
20
 
13
21
  async function main() {
14
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
15
23
  const templatesRoot = path.join(__dirname, "../templates")
24
+ const cliOptions = parseCliOptions(process.argv.slice(USER_ARG_START_INDEX))
16
25
 
17
26
  let canceled = false
18
27
 
@@ -71,6 +80,8 @@ async function main() {
71
80
  }
72
81
 
73
82
  let renderer = "lit-html"
83
+ let pwa = cliOptions.pwa === true
84
+ let mobile = cliOptions.mobile === true
74
85
 
75
86
  // Ask about renderer for non-minimal templates
76
87
  if (baseTemplate !== "minimal") {
@@ -108,6 +119,48 @@ async function main() {
108
119
  renderer = selectedRenderer
109
120
  }
110
121
 
122
+ const supportsAppFeatures = VITE_APP_TEMPLATES.has(baseTemplate)
123
+
124
+ if (supportsAppFeatures) {
125
+ const featureQuestions = []
126
+
127
+ if (cliOptions.pwa === null) {
128
+ featureQuestions.push({
129
+ type: "confirm",
130
+ name: "pwa",
131
+ message: "Enable PWA support?",
132
+ initial: false,
133
+ })
134
+ }
135
+
136
+ if (cliOptions.mobile === null) {
137
+ featureQuestions.push({
138
+ type: "confirm",
139
+ name: "mobile",
140
+ message: "Add hybrid mobile support with Capacitor?",
141
+ initial: false,
142
+ })
143
+ }
144
+
145
+ if (featureQuestions.length > EMPTY_LENGTH) {
146
+ const answers = await prompts(featureQuestions, { onCancel })
147
+
148
+ if (canceled) {
149
+ console.log("Operation canceled.")
150
+ return
151
+ }
152
+
153
+ pwa = cliOptions.pwa === true ? true : answers.pwa === true
154
+ mobile = cliOptions.mobile === true ? true : answers.mobile === true
155
+ }
156
+ } else if (pwa || mobile) {
157
+ console.warn(
158
+ "PWA and mobile options are only available for JavaScript and TypeScript Vite app templates.",
159
+ )
160
+ pwa = false
161
+ mobile = false
162
+ }
163
+
111
164
  const spinner = ora(`Creating project "${projectName}"...`).start()
112
165
 
113
166
  try {
@@ -143,6 +196,16 @@ async function main() {
143
196
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
144
197
  pkg.name = projectName
145
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
+
146
209
  for (const depType of ["dependencies", "devDependencies"]) {
147
210
  if (pkg[depType]) {
148
211
  for (const [name, version] of Object.entries(pkg[depType])) {
@@ -195,6 +258,7 @@ async function main() {
195
258
  const version = execSync(`npm view ${packageName} version`, {
196
259
  encoding: "utf-8",
197
260
  stdio: "pipe", // Prevent npm view output from cluttering the console
261
+ timeout: NPM_VIEW_TIMEOUT_MS,
198
262
  }).trim()
199
263
  return `^${version}`
200
264
  } catch {
@@ -203,6 +267,231 @@ async function main() {
203
267
  }
204
268
  }
205
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
+
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")
360
+ }
361
+
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
+ }
430
+
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}`
435
+ }
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
+ }
448
+
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
+ `
463
+ }
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
+ }
494
+
206
495
  main().catch((e) => {
207
496
  console.error(e)
208
497
  })
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@inglorious/create-app",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "A scaffolding tool to quickly create a new app with Inglorious Web.",
5
- "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
5
+ "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore } from "@inglorious/web"
1
+ import { createStore } from "@inglorious/store"
2
2
 
3
3
  import { Message } from "../types/message"
4
4
  import { entities } from "./entities"
@@ -1,4 +1,4 @@
1
- import { createDevtools } from "@inglorious/web"
1
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
2
 
3
3
  export const middlewares = []
4
4
 
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore } from "@inglorious/web"
1
+ import { createStore } from "@inglorious/store"
2
2
 
3
3
  import { Message } from "../types/message"
4
4
  import { entities } from "./entities"
@@ -1,4 +1,4 @@
1
- import { createDevtools } from "@inglorious/web"
1
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
2
 
3
3
  export const middlewares = []
4
4
 
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore } from "@inglorious/web"
1
+ import { createStore } from "@inglorious/store"
2
2
 
3
3
  import { Message } from "../types/message.vue"
4
4
  import { entities } from "./entities"
@@ -1,4 +1,4 @@
1
- import { createDevtools } from "@inglorious/web"
1
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
2
 
3
3
  export const middlewares = []
4
4
 
@@ -24,7 +24,8 @@
24
24
  "@inglorious/utils/": "https://unpkg.com/@inglorious%2Futils@latest/src/",
25
25
  "@inglorious/store": "https://unpkg.com/@inglorious%2Fstore@latest/src/store.js",
26
26
  "@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@latest/src/",
27
- "@inglorious/web": "https://unpkg.com/@inglorious%2Fweb@latest/src/index.js"
27
+ "@inglorious/web": "https://unpkg.com/@inglorious%2Fweb@latest/src/index.js",
28
+ "@inglorious/web/": "https://unpkg.com/@inglorious%2Fweb@latest/src/"
28
29
  }
29
30
  }
30
31
  </script>
@@ -1,4 +1,4 @@
1
- import { createStore } from "@inglorious/web"
1
+ import { createStore } from "@inglorious/store"
2
2
 
3
3
  import { Message } from "../types/message.js"
4
4
  import { entities } from "./entities.js"
@@ -1,4 +1,4 @@
1
- import { createDevtools } from "@inglorious/web"
1
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
2
 
3
3
  export const middlewares = []
4
4
 
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore, type TypesConfig } from "@inglorious/web"
1
+ import { createStore, type TypesConfig } from "@inglorious/store"
2
2
 
3
3
  import type { AppEntity, AppState } from "../../types"
4
4
  import { Message } from "../types/message"
@@ -1,4 +1,5 @@
1
- import { createDevtools, type Middleware } from "@inglorious/web"
1
+ import type { Middleware } from "@inglorious/store"
2
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
3
 
3
4
  import type { AppEntity, AppState } from "../../types"
4
5
 
@@ -1,6 +1,6 @@
1
1
  import "./vite-end"
2
2
 
3
- import { BaseEntity, EntitiesState } from "@inglorious/web"
3
+ import { BaseEntity, EntitiesState } from "@inglorious/store"
4
4
 
5
5
  // Define your entity types
6
6
  export interface MessageEntity extends BaseEntity {
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore, type TypesConfig } from "@inglorious/web"
1
+ import { createStore, type TypesConfig } from "@inglorious/store"
2
2
 
3
3
  import type { AppEntity, AppState } from "../../types"
4
4
  import { Message } from "../types/message"
@@ -1,4 +1,5 @@
1
- import { createDevtools, type Middleware } from "@inglorious/web"
1
+ import type { Middleware } from "@inglorious/store"
2
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
3
 
3
4
  import type { AppEntity, AppState } from "../../types"
4
5
 
@@ -1,6 +1,7 @@
1
1
  import "./vite-end"
2
2
 
3
- import { Api, BaseEntity, EntitiesState, TemplateResult } from "@inglorious/web"
3
+ import { BaseEntity, EntitiesState } from "@inglorious/store"
4
+ import { Api, TemplateResult } from "@inglorious/web"
4
5
 
5
6
  // Define your entity types
6
7
  export interface MessageEntity extends BaseEntity {
@@ -14,6 +14,7 @@
14
14
  "preview": "vite preview"
15
15
  },
16
16
  "dependencies": {
17
+ "@inglorious/store": "workspace:*",
17
18
  "@inglorious/web": "workspace:*"
18
19
  },
19
20
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { createStore, type TypesConfig } from "@inglorious/web"
1
+ import { createStore, type TypesConfig } from "@inglorious/store"
2
2
 
3
3
  import type { AppEntity, AppState } from "../../types"
4
4
  // @ts-expect-error - Handled by @inglorious/vite-plugin-vue
@@ -1,4 +1,4 @@
1
- import { createDevtools } from "@inglorious/web"
1
+ import { createDevtools } from "@inglorious/store/client/devtools"
2
2
 
3
3
  export const middlewares = []
4
4
 
@@ -1,6 +1,7 @@
1
1
  import "./vite-end"
2
2
 
3
- import { Api, BaseEntity, EntitiesState, TemplateResult } from "@inglorious/web"
3
+ import { BaseEntity, EntitiesState } from "@inglorious/store"
4
+ import { Api, TemplateResult } from "@inglorious/web"
4
5
 
5
6
  // Define your entity types
6
7
  export interface MessageEntity extends BaseEntity {