@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.
Files changed (46) hide show
  1. package/README.md +25 -8
  2. package/bin/index.js +79 -401
  3. package/bin/lib/app-jsx.js +280 -0
  4. package/bin/lib/app-lit.js +275 -0
  5. package/bin/lib/app-vue.js +239 -0
  6. package/bin/lib/app.js +402 -0
  7. package/bin/lib/cli.js +49 -0
  8. package/bin/lib/constants.js +14 -0
  9. package/bin/lib/features.js +142 -0
  10. package/bin/lib/file.js +88 -0
  11. package/bin/lib/imports.js +64 -0
  12. package/bin/lib/mobile.js +140 -0
  13. package/bin/lib/pwa.js +168 -0
  14. package/bin/lib/questions.js +184 -0
  15. package/bin/lib/source.js +58 -0
  16. package/package.json +4 -4
  17. package/templates/js/package.json +8 -7
  18. package/templates/js/vite.config.js +2 -1
  19. package/templates/js-jsx/package.json +8 -7
  20. package/templates/js-jsx/vite.config.js +2 -1
  21. package/templates/js-vue/package.json +7 -6
  22. package/templates/js-vue/vite.config.js +2 -1
  23. package/templates/ssx-js/package.json +8 -7
  24. package/templates/ssx-js/src/site.config.js +2 -1
  25. package/templates/ssx-ts/package.json +9 -7
  26. package/templates/ssx-ts/src/site.config.ts +1 -0
  27. package/templates/ts/package.json +10 -8
  28. package/templates/ts/src/app.ts +4 -2
  29. package/templates/ts/tsconfig.app.json +2 -0
  30. package/templates/ts/tsconfig.node.json +2 -0
  31. package/templates/ts/types/index.d.ts +4 -1
  32. package/templates/ts/vite.config.ts +2 -1
  33. package/templates/ts-jsx/package.json +10 -8
  34. package/templates/ts-jsx/src/app.tsx +2 -2
  35. package/templates/ts-jsx/tsconfig.app.json +2 -0
  36. package/templates/ts-jsx/tsconfig.node.json +2 -0
  37. package/templates/ts-jsx/types/index.d.ts +3 -1
  38. package/templates/ts-jsx/types/vite-end.d.ts +1 -0
  39. package/templates/ts-jsx/vite.config.ts +2 -1
  40. package/templates/ts-vue/eslint.config.ts +1 -1
  41. package/templates/ts-vue/package.json +10 -8
  42. package/templates/ts-vue/src/store/middlewares.ts +5 -2
  43. package/templates/ts-vue/tsconfig.app.json +2 -0
  44. package/templates/ts-vue/tsconfig.node.json +2 -0
  45. package/templates/ts-vue/types/index.d.ts +3 -1
  46. 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
+ }
@@ -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
+ }