@inglorious/create-app 1.1.1 → 1.2.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 +25 -8
- package/bin/index.js +79 -401
- package/bin/lib/app-jsx.js +280 -0
- package/bin/lib/app-lit.js +275 -0
- package/bin/lib/app-vue.js +239 -0
- package/bin/lib/app.js +402 -0
- package/bin/lib/cli.js +49 -0
- package/bin/lib/constants.js +14 -0
- package/bin/lib/features.js +142 -0
- package/bin/lib/file.js +88 -0
- package/bin/lib/imports.js +64 -0
- package/bin/lib/mobile.js +140 -0
- package/bin/lib/pwa.js +168 -0
- package/bin/lib/questions.js +184 -0
- package/bin/lib/source.js +58 -0
- package/package.json +4 -4
- package/templates/js/package.json +8 -7
- package/templates/js/vite.config.js +2 -1
- package/templates/js-jsx/package.json +8 -7
- package/templates/js-jsx/vite.config.js +2 -1
- package/templates/js-vue/package.json +7 -6
- package/templates/js-vue/vite.config.js +2 -1
- package/templates/ssx-js/package.json +8 -7
- package/templates/ssx-js/src/site.config.js +2 -1
- package/templates/ssx-ts/package.json +9 -7
- package/templates/ssx-ts/src/site.config.ts +1 -0
- package/templates/ts/package.json +10 -8
- package/templates/ts/src/app.ts +4 -2
- package/templates/ts/tsconfig.app.json +2 -0
- package/templates/ts/tsconfig.node.json +2 -0
- package/templates/ts/types/index.d.ts +4 -1
- package/templates/ts/vite.config.ts +2 -1
- package/templates/ts-jsx/package.json +10 -8
- package/templates/ts-jsx/src/app.tsx +2 -2
- package/templates/ts-jsx/tsconfig.app.json +2 -0
- package/templates/ts-jsx/tsconfig.node.json +2 -0
- package/templates/ts-jsx/types/index.d.ts +3 -1
- package/templates/ts-jsx/types/vite-end.d.ts +1 -0
- package/templates/ts-jsx/vite.config.ts +2 -1
- package/templates/ts-vue/eslint.config.ts +1 -1
- package/templates/ts-vue/package.json +10 -8
- package/templates/ts-vue/src/store/middlewares.ts +5 -2
- package/templates/ts-vue/tsconfig.app.json +2 -0
- package/templates/ts-vue/tsconfig.node.json +2 -0
- package/templates/ts-vue/types/index.d.ts +3 -1
- package/templates/ts-vue/vite.config.ts +2 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The choices create-app offers for lit-html apps, and what each one brings.
|
|
2
|
+
|
|
3
|
+
export const THEMES = {
|
|
4
|
+
inglorious: { title: "Inglorious", description: "Neon, videogame-inspired" },
|
|
5
|
+
material: { title: "Material", description: "Material Design inspired" },
|
|
6
|
+
bootstrap: { title: "Bootstrap", description: "Bootstrap inspired" },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Icon sets from Iconify: one module per icon, so only the icons an app
|
|
10
|
+
// imports are bundled. `nav` names the icons for the two example pages,
|
|
11
|
+
// `showcase` the one shown on the home page.
|
|
12
|
+
export const ICON_PACKS = {
|
|
13
|
+
material: {
|
|
14
|
+
title: "Material Symbols",
|
|
15
|
+
packages: { "@iconify-icons/material-symbols": "^2.0.6" },
|
|
16
|
+
nav: {
|
|
17
|
+
home: from("homeOutline", "@iconify-icons/material-symbols/home-outline"),
|
|
18
|
+
about: from(
|
|
19
|
+
"infoOutline",
|
|
20
|
+
"@iconify-icons/material-symbols/info-outline",
|
|
21
|
+
),
|
|
22
|
+
},
|
|
23
|
+
showcase: {
|
|
24
|
+
...from(
|
|
25
|
+
"favoriteOutline",
|
|
26
|
+
"@iconify-icons/material-symbols/favorite-outline",
|
|
27
|
+
),
|
|
28
|
+
label: "Material Symbols",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
fontawesome: {
|
|
32
|
+
title: "Font Awesome",
|
|
33
|
+
packages: {
|
|
34
|
+
"@iconify-icons/fa7-solid": "^2.0.0",
|
|
35
|
+
"@iconify-icons/fa7-regular": "^2.0.0",
|
|
36
|
+
"@iconify-icons/fa7-brands": "^2.0.0",
|
|
37
|
+
},
|
|
38
|
+
nav: {
|
|
39
|
+
home: from("house", "@iconify-icons/fa7-solid/house"),
|
|
40
|
+
about: from("circleInfo", "@iconify-icons/fa7-solid/circle-info"),
|
|
41
|
+
},
|
|
42
|
+
showcase: {
|
|
43
|
+
...from("fontAwesome", "@iconify-icons/fa7-brands/font-awesome"),
|
|
44
|
+
label: "Font Awesome",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
lucide: {
|
|
48
|
+
title: "Lucide",
|
|
49
|
+
packages: { "@iconify-icons/lucide": "^2.0.22" },
|
|
50
|
+
nav: {
|
|
51
|
+
home: from("house", "@iconify-icons/lucide/house"),
|
|
52
|
+
about: from("info", "@iconify-icons/lucide/info"),
|
|
53
|
+
},
|
|
54
|
+
showcase: {
|
|
55
|
+
...from("sparkles", "@iconify-icons/lucide/sparkles"),
|
|
56
|
+
label: "Lucide",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The languages offered for the example app, with its few texts in each.
|
|
62
|
+
export const LANGUAGES = {
|
|
63
|
+
en: language("English", {
|
|
64
|
+
nav: { home: "Home", about: "About" },
|
|
65
|
+
language: "Language",
|
|
66
|
+
home: { title: "Welcome", hint: "Click a greeting to shout it." },
|
|
67
|
+
about: { title: "About", body: "A page of its own, at its own address." },
|
|
68
|
+
message: { hello: "Hello {{who}}" },
|
|
69
|
+
}),
|
|
70
|
+
it: language("Italiano", {
|
|
71
|
+
nav: { home: "Home", about: "Info" },
|
|
72
|
+
language: "Lingua",
|
|
73
|
+
home: { title: "Benvenuto", hint: "Clicca un saluto per urlarlo." },
|
|
74
|
+
about: { title: "Info", body: "Una pagina tutta sua, al suo indirizzo." },
|
|
75
|
+
message: { hello: "Ciao {{who}}" },
|
|
76
|
+
}),
|
|
77
|
+
fr: language("Français", {
|
|
78
|
+
nav: { home: "Accueil", about: "À propos" },
|
|
79
|
+
language: "Langue",
|
|
80
|
+
home: { title: "Bienvenue", hint: "Cliquez sur un salut pour le crier." },
|
|
81
|
+
about: {
|
|
82
|
+
title: "À propos",
|
|
83
|
+
body: "Une page à part entière, à sa propre adresse.",
|
|
84
|
+
},
|
|
85
|
+
message: { hello: "Bonjour {{who}}" },
|
|
86
|
+
}),
|
|
87
|
+
de: language("Deutsch", {
|
|
88
|
+
nav: { home: "Start", about: "Über" },
|
|
89
|
+
language: "Sprache",
|
|
90
|
+
home: {
|
|
91
|
+
title: "Willkommen",
|
|
92
|
+
hint: "Klicke auf einen Gruß, um ihn zu rufen.",
|
|
93
|
+
},
|
|
94
|
+
about: { title: "Über", body: "Eine eigene Seite unter eigener Adresse." },
|
|
95
|
+
message: { hello: "Hallo {{who}}" },
|
|
96
|
+
}),
|
|
97
|
+
es: language("Español", {
|
|
98
|
+
nav: { home: "Inicio", about: "Acerca de" },
|
|
99
|
+
language: "Idioma",
|
|
100
|
+
home: { title: "Bienvenido", hint: "Haz clic en un saludo para gritarlo." },
|
|
101
|
+
about: {
|
|
102
|
+
title: "Acerca de",
|
|
103
|
+
body: "Una página propia, en su propia dirección.",
|
|
104
|
+
},
|
|
105
|
+
message: { hello: "Hola {{who}}" },
|
|
106
|
+
}),
|
|
107
|
+
pt: language("Português", {
|
|
108
|
+
nav: { home: "Início", about: "Sobre" },
|
|
109
|
+
language: "Idioma",
|
|
110
|
+
home: { title: "Bem-vindo", hint: "Clique numa saudação para gritá-la." },
|
|
111
|
+
about: {
|
|
112
|
+
title: "Sobre",
|
|
113
|
+
body: "Uma página própria, no seu próprio endereço.",
|
|
114
|
+
},
|
|
115
|
+
message: { hello: "Olá {{who}}" },
|
|
116
|
+
}),
|
|
117
|
+
ja: language("日本語", {
|
|
118
|
+
nav: { home: "ホーム", about: "概要" },
|
|
119
|
+
language: "言語",
|
|
120
|
+
home: {
|
|
121
|
+
title: "ようこそ",
|
|
122
|
+
hint: "あいさつをクリックすると大きくなります。",
|
|
123
|
+
},
|
|
124
|
+
about: { title: "概要", body: "専用のアドレスを持つ独立したページです。" },
|
|
125
|
+
message: { hello: "こんにちは、{{who}}" },
|
|
126
|
+
}),
|
|
127
|
+
ar: language("العربية", {
|
|
128
|
+
nav: { home: "الرئيسية", about: "حول" },
|
|
129
|
+
language: "اللغة",
|
|
130
|
+
home: { title: "مرحبًا", hint: "انقر على تحية لتكبيرها." },
|
|
131
|
+
about: { title: "حول", body: "صفحة مستقلة بعنوانها الخاص." },
|
|
132
|
+
message: { hello: "مرحبًا {{who}}" },
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function from(name, source) {
|
|
137
|
+
return { name, from: source }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function language(title, texts) {
|
|
141
|
+
return { title, texts }
|
|
142
|
+
}
|
package/bin/lib/file.js
ADDED
|
@@ -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,64 @@
|
|
|
1
|
+
// Import statements in the order eslint-plugin-simple-import-sort gives them,
|
|
2
|
+
// so a generated project passes `eslint` as it is: side-effect imports first,
|
|
3
|
+
// in the order given, then packages, then relative modules, a blank line
|
|
4
|
+
// between groups.
|
|
5
|
+
|
|
6
|
+
const collator = new Intl.Collator("en", { sensitivity: "base", numeric: true })
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} lines - One import statement each.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function importBlock(lines) {
|
|
13
|
+
const sideEffects = []
|
|
14
|
+
const packages = []
|
|
15
|
+
const relative = []
|
|
16
|
+
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const source = sourceOf(line)
|
|
19
|
+
if (!line.includes(" from ")) sideEffects.push(line)
|
|
20
|
+
else if (source.startsWith(".")) relative.push(line)
|
|
21
|
+
else packages.push(line)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [sideEffects, sortImports(packages), sortImports(relative)]
|
|
25
|
+
.filter((group) => group.length)
|
|
26
|
+
.map((group) => group.join("\n"))
|
|
27
|
+
.join("\n\n")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortImports(lines) {
|
|
31
|
+
return lines
|
|
32
|
+
.map((line) => ({ line, source: sourceOf(line) }))
|
|
33
|
+
.sort(
|
|
34
|
+
(a, b) =>
|
|
35
|
+
compare(normalize(a.source), normalize(b.source)) ||
|
|
36
|
+
compare(a.source, b.source) ||
|
|
37
|
+
// Type imports before regular ones from the same module.
|
|
38
|
+
Number(b.line.startsWith("import type")) -
|
|
39
|
+
Number(a.line.startsWith("import type")),
|
|
40
|
+
)
|
|
41
|
+
.map(({ line }) => line)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The plugin's own normalization: by directory level, `.` and `/` before any
|
|
45
|
+
// other punctuation.
|
|
46
|
+
function normalize(source) {
|
|
47
|
+
return source
|
|
48
|
+
.replace(/^[./]*\.$/, "$&/")
|
|
49
|
+
.replace(/^[./]*\/$/, "$&,")
|
|
50
|
+
.replace(
|
|
51
|
+
/[./_-]/g,
|
|
52
|
+
(char) => ({ ".": "_", "/": "-", _: ".", "-": "/" })[char],
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function compare(a, b) {
|
|
57
|
+
return collator.compare(a, b) || Number(a > b) - Number(a < b)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The module an import statement imports from.
|
|
61
|
+
function sourceOf(line) {
|
|
62
|
+
const [, source] = line.match(/"([^"]+)"/)
|
|
63
|
+
return source
|
|
64
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
// For capacitor.config.ts. A TypeScript template already has its own, which
|
|
18
|
+
// its code is written against.
|
|
19
|
+
pkg.devDependencies["typescript"] ??= TYPESCRIPT_VERSION
|
|
20
|
+
|
|
21
|
+
pkg.scripts.cap = "cap"
|
|
22
|
+
pkg.scripts["mobile:init:ios"] = "cap add ios"
|
|
23
|
+
pkg.scripts["mobile:init:android"] = "cap add android"
|
|
24
|
+
pkg.scripts["mobile:sync"] = "pnpm build && cap sync"
|
|
25
|
+
pkg.scripts["mobile:ios"] = "pnpm build && cap run ios"
|
|
26
|
+
pkg.scripts["mobile:android"] = "pnpm build && cap run android"
|
|
27
|
+
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(targetDir, "capacitor.config.ts"),
|
|
30
|
+
createCapacitorConfigSource(projectName),
|
|
31
|
+
)
|
|
32
|
+
includeInNodeProject(targetDir, "capacitor.config.ts")
|
|
33
|
+
|
|
34
|
+
appendMobileSetupToReadme(targetDir)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A TypeScript template type-checks and lints its config files through
|
|
38
|
+
// tsconfig.node.json, so a new one has to be listed there. Edited as text, so
|
|
39
|
+
// the `include` line keeps the one-line layout Prettier gives it.
|
|
40
|
+
function includeInNodeProject(targetDir, file) {
|
|
41
|
+
const tsconfigPath = path.join(targetDir, "tsconfig.node.json")
|
|
42
|
+
if (!fs.existsSync(tsconfigPath)) return
|
|
43
|
+
|
|
44
|
+
const tsconfig = fs.readFileSync(tsconfigPath, "utf-8")
|
|
45
|
+
if (tsconfig.includes(`"${file}"`)) return
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
tsconfigPath,
|
|
48
|
+
tsconfig.replace(/("include": \[[^\]]*)\]/, `$1, "${file}"]`),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function appendMobileSetupToReadme(targetDir) {
|
|
53
|
+
const readmePath = path.join(targetDir, "README.md")
|
|
54
|
+
if (!fs.existsSync(readmePath)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const readme = fs.readFileSync(readmePath, "utf-8")
|
|
59
|
+
if (readme.includes("## Mobile Setup")) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const mobileReadme = `
|
|
64
|
+
## Mobile Setup
|
|
65
|
+
|
|
66
|
+
This project includes Capacitor for Android and iOS support.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
### Android
|
|
71
|
+
|
|
72
|
+
Before you run the native apps, make sure you have:
|
|
73
|
+
|
|
74
|
+
- Android Studio installed with the Android SDK: https://developer.android.com/studio
|
|
75
|
+
- 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.
|
|
76
|
+
- The SDK path available via ANDROID_HOME (for example, on macOS this is often $HOME/Library/Android/sdk)
|
|
77
|
+
|
|
78
|
+
On macOS, the following shell variables are commonly needed:
|
|
79
|
+
|
|
80
|
+
\`\`\`bash
|
|
81
|
+
export JAVA_HOME="/path/to/jdk-21"
|
|
82
|
+
export ANDROID_HOME="$HOME/Library/Android/sdk"
|
|
83
|
+
export PATH="$ANDROID_HOME/platform-tools:$PATH"
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
### iOS
|
|
87
|
+
|
|
88
|
+
For iOS you also need the Apple toolchain and device setup on a Mac:
|
|
89
|
+
|
|
90
|
+
- Xcode from the App Store
|
|
91
|
+
- iOS development tools and simulator components (this can be very large, often multiple GB)
|
|
92
|
+
- A valid Apple Developer team / signing identity in Xcode
|
|
93
|
+
- Device trust setup for your iPhone in Settings > General > VPN & Device Management
|
|
94
|
+
|
|
95
|
+
The most common first-time setup commands are:
|
|
96
|
+
|
|
97
|
+
\`\`\`bash
|
|
98
|
+
sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
|
|
99
|
+
sudo xcodebuild -license accept
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
### Official resources
|
|
105
|
+
|
|
106
|
+
- Android: https://capacitorjs.com/docs/android
|
|
107
|
+
- iOS: https://capacitorjs.com/docs/ios
|
|
108
|
+
- Android Studio: https://developer.android.com/studio
|
|
109
|
+
- Xcode: https://developer.apple.com/xcode/
|
|
110
|
+
|
|
111
|
+
Then sync and open the native project:
|
|
112
|
+
|
|
113
|
+
\`\`\`bash
|
|
114
|
+
pnpm mobile:sync
|
|
115
|
+
pnpm mobile:android
|
|
116
|
+
# or
|
|
117
|
+
pnpm mobile:ios
|
|
118
|
+
\`\`\`
|
|
119
|
+
`
|
|
120
|
+
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
readmePath,
|
|
123
|
+
`${readme.trimEnd()}${EOL}${EOL}${mobileReadme.trim()}${EOL}`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createCapacitorConfigSource(projectName) {
|
|
128
|
+
const appId = `com.inglorious.${toJavaIdentifier(projectName)}`
|
|
129
|
+
|
|
130
|
+
return `import type { CapacitorConfig } from "@capacitor/cli"
|
|
131
|
+
|
|
132
|
+
const config: CapacitorConfig = {
|
|
133
|
+
appId: "${appId}",
|
|
134
|
+
appName: "${projectName}",
|
|
135
|
+
webDir: "dist",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default config
|
|
139
|
+
`
|
|
140
|
+
}
|
package/bin/lib/pwa.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
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 = injectSideEffectImport(main, `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
|
+
// Right after the image optimizer, where the imports' alphabetical order
|
|
54
|
+
// puts it.
|
|
55
|
+
viteConfig = injectAfterImport(
|
|
56
|
+
viteConfig,
|
|
57
|
+
viteConfig.includes("vite-plugin-image-optimizer")
|
|
58
|
+
? "vite-plugin-image-optimizer"
|
|
59
|
+
: "vite",
|
|
60
|
+
`import { VitePWA } from "vite-plugin-pwa"`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!viteConfig.includes("VitePWA(")) {
|
|
65
|
+
// One plugin per line, as Prettier prints a list this long.
|
|
66
|
+
viteConfig = viteConfig.replace(
|
|
67
|
+
/plugins: \[(.*)\],/,
|
|
68
|
+
(_, plugins) =>
|
|
69
|
+
`plugins: [${EOL}${[
|
|
70
|
+
createVitePwaConfig(projectName),
|
|
71
|
+
...splitPlugins(plugins),
|
|
72
|
+
]
|
|
73
|
+
.map((plugin) => ` ${plugin},`)
|
|
74
|
+
.join(EOL)}${EOL} ],`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(viteConfigPath, viteConfig)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writePwaRegistration(targetDir) {
|
|
82
|
+
const mainPath = findMainPath(targetDir)
|
|
83
|
+
|
|
84
|
+
if (!mainPath) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const extension = path.extname(mainPath)
|
|
89
|
+
const pwaPath = path.join(targetDir, `src/pwa${extension}`)
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(pwaPath, createPwaRegistrationSource(extension))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createPwaRegistrationSource(extension) {
|
|
95
|
+
if (extension === ".ts") {
|
|
96
|
+
return `/// <reference types="vite-plugin-pwa/client" />
|
|
97
|
+
import { isNative } from "@inglorious/web/mobile"
|
|
98
|
+
import { registerSW } from "virtual:pwa-register"
|
|
99
|
+
|
|
100
|
+
if (!isNative()) {
|
|
101
|
+
registerSW()
|
|
102
|
+
}
|
|
103
|
+
`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `import { isNative } from "@inglorious/web/mobile"
|
|
107
|
+
import { registerSW } from "virtual:pwa-register"
|
|
108
|
+
|
|
109
|
+
if (!isNative()) {
|
|
110
|
+
registerSW()
|
|
111
|
+
}
|
|
112
|
+
`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createVitePwaConfig(projectName) {
|
|
116
|
+
return `VitePWA({
|
|
117
|
+
injectRegister: false,
|
|
118
|
+
registerType: "autoUpdate",
|
|
119
|
+
pwaAssets: {
|
|
120
|
+
image: "public/logo.png",
|
|
121
|
+
preset: "minimal-2023",
|
|
122
|
+
},
|
|
123
|
+
manifest: {
|
|
124
|
+
name: "${projectName}",
|
|
125
|
+
short_name: "${projectName}",
|
|
126
|
+
description: "An Inglorious Web app.",
|
|
127
|
+
theme_color: "#ffffff",
|
|
128
|
+
background_color: "#ffffff",
|
|
129
|
+
display: "standalone",
|
|
130
|
+
start_url: "/",
|
|
131
|
+
},
|
|
132
|
+
})`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The plugins of a one-line `plugins: [a(), b({ c: 1 })]`, split at the
|
|
136
|
+
// top-level commas.
|
|
137
|
+
function splitPlugins(source) {
|
|
138
|
+
const plugins = []
|
|
139
|
+
let depth = 0
|
|
140
|
+
let current = ""
|
|
141
|
+
for (const char of source) {
|
|
142
|
+
if ("([{".includes(char)) depth++
|
|
143
|
+
if (")]}".includes(char)) depth--
|
|
144
|
+
if (char === "," && !depth) {
|
|
145
|
+
plugins.push(current.trim())
|
|
146
|
+
current = ""
|
|
147
|
+
} else {
|
|
148
|
+
current += char
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (current.trim()) plugins.push(current.trim())
|
|
152
|
+
return plugins
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A side-effect import goes with the others at the top, in the order they
|
|
156
|
+
// come, as eslint-plugin-simple-import-sort keeps them.
|
|
157
|
+
function injectSideEffectImport(source, newImport) {
|
|
158
|
+
const lastSideEffect = source
|
|
159
|
+
.split(EOL)
|
|
160
|
+
.findLast((line) => /^import ["']/.test(line))
|
|
161
|
+
if (lastSideEffect) {
|
|
162
|
+
return source.replace(
|
|
163
|
+
`${lastSideEffect}${EOL}`,
|
|
164
|
+
`${lastSideEffect}${EOL}${newImport}${EOL}`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
return `${newImport}${EOL}${EOL}${source}`
|
|
168
|
+
}
|