@inglorious/create-app 1.1.2 → 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 (45) hide show
  1. package/README.md +15 -7
  2. package/bin/index.js +34 -39
  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 +31 -0
  8. package/bin/lib/constants.js +3 -3
  9. package/bin/lib/features.js +142 -0
  10. package/bin/lib/imports.js +64 -0
  11. package/bin/lib/mobile.js +19 -1
  12. package/bin/lib/pwa.js +50 -4
  13. package/bin/lib/questions.js +184 -0
  14. package/bin/lib/source.js +58 -0
  15. package/package.json +3 -3
  16. package/templates/js/package.json +4 -4
  17. package/templates/js/vite.config.js +2 -1
  18. package/templates/js-jsx/package.json +4 -4
  19. package/templates/js-jsx/vite.config.js +2 -1
  20. package/templates/js-vue/package.json +4 -4
  21. package/templates/js-vue/vite.config.js +2 -1
  22. package/templates/ssx-js/package.json +8 -7
  23. package/templates/ssx-js/src/site.config.js +2 -1
  24. package/templates/ssx-ts/package.json +9 -7
  25. package/templates/ssx-ts/src/site.config.ts +1 -0
  26. package/templates/ts/package.json +7 -6
  27. package/templates/ts/src/app.ts +4 -2
  28. package/templates/ts/tsconfig.app.json +2 -0
  29. package/templates/ts/tsconfig.node.json +2 -0
  30. package/templates/ts/types/index.d.ts +4 -1
  31. package/templates/ts/vite.config.ts +2 -1
  32. package/templates/ts-jsx/package.json +7 -6
  33. package/templates/ts-jsx/src/app.tsx +2 -2
  34. package/templates/ts-jsx/tsconfig.app.json +2 -0
  35. package/templates/ts-jsx/tsconfig.node.json +2 -0
  36. package/templates/ts-jsx/types/index.d.ts +3 -1
  37. package/templates/ts-jsx/types/vite-end.d.ts +1 -0
  38. package/templates/ts-jsx/vite.config.ts +2 -1
  39. package/templates/ts-vue/eslint.config.ts +1 -1
  40. package/templates/ts-vue/package.json +7 -6
  41. package/templates/ts-vue/src/store/middlewares.ts +5 -2
  42. package/templates/ts-vue/tsconfig.app.json +2 -0
  43. package/templates/ts-vue/tsconfig.node.json +2 -0
  44. package/templates/ts-vue/types/index.d.ts +3 -1
  45. package/templates/ts-vue/vite.config.ts +2 -1
package/README.md CHANGED
@@ -23,17 +23,25 @@ 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:
26
+ For the JavaScript and TypeScript Vite templates, the CLI then asks about the app's features:
27
+
28
+ | Question | Flag | Adds |
29
+ | ------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
30
+ | Use Inglorious UI? | `--ui` / `--no-ui` | `@inglorious/ui`, its base styles and an app bar |
31
+ | Which theme? | `--theme=inglorious\|material\|bootstrap` | The theme's stylesheet and class |
32
+ | Is it a mobile app? | `--mobile` / `--no-mobile` | Capacitor for Android and iOS; with Inglorious UI, a bottom tab bar and safe-area spacing |
33
+ | Add routing? | `--routing` / `--no-routing` | The Router, a home and an about page, and navigation between them |
34
+ | Animate page transitions? | `--transitions` / `--no-transitions` | View transitions between pages (with routing and Inglorious UI) |
35
+ | Which icon packs? | `--icons=material,fontawesome,lucide` / `--no-icons` | Iconify icon sets, drawn by Inglorious UI's `Icon` (with Inglorious UI) |
36
+ | Translate the app? | `--i18n` / `--no-i18n`, `--languages=en,it` | `@inglorious/i18n`, a catalogue per language and a language picker; the first language is the default |
37
+ | Enable PWA support? | `--pwa` / `--no-pwa` | `vite-plugin-pwa`, a manifest, icons and service worker registration |
38
+
39
+ The app is generated in the syntax you chose: lit-html, JSX or Vue. Every question answered by a flag is skipped:
27
40
 
28
41
  ```bash
29
- npm create @inglorious/app@latest -- --pwa
30
- npm create @inglorious/app@latest -- --mobile
31
- npm create @inglorious/app@latest -- --pwa --mobile
42
+ npm create @inglorious/app@latest -- --ui --theme=material --routing --transitions --icons=material --languages=en,it --mobile
32
43
  ```
33
44
 
34
- - `--pwa` adds `vite-plugin-pwa`, a basic web app manifest configuration, PWA icons, and browser-only service worker registration.
35
- - `--mobile` adds Capacitor dependencies, config, and mobile scripts for iOS and Android.
36
-
37
45
  ## Templates
38
46
 
39
47
  The following templates are available to get you started:
package/bin/index.js CHANGED
@@ -8,9 +8,9 @@ import path from "path"
8
8
  import prompts from "prompts"
9
9
  import { fileURLToPath } from "url"
10
10
 
11
+ import { addAppFeatures } from "./lib/app.js"
11
12
  import { parseCliOptions } from "./lib/cli.js"
12
13
  import {
13
- EMPTY_LENGTH,
14
14
  INDENTATION,
15
15
  NPM_VIEW_TIMEOUT_MS,
16
16
  USER_ARG_START_INDEX,
@@ -19,6 +19,7 @@ import {
19
19
  import { copyDir } from "./lib/file.js"
20
20
  import { addMobileSupport } from "./lib/mobile.js"
21
21
  import { addPwaSupport } from "./lib/pwa.js"
22
+ import { askAppFeatures } from "./lib/questions.js"
22
23
 
23
24
  async function main() {
24
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -82,8 +83,6 @@ async function main() {
82
83
  }
83
84
 
84
85
  let renderer = "lit-html"
85
- let pwa = cliOptions.pwa === true
86
- let mobile = cliOptions.mobile === true
87
86
 
88
87
  if (baseTemplate !== "minimal") {
89
88
  const { selectedRenderer } = await prompts(
@@ -120,46 +119,26 @@ async function main() {
120
119
  renderer = selectedRenderer
121
120
  }
122
121
 
123
- const supportsAppFeatures = VITE_APP_TEMPLATES.has(baseTemplate)
124
-
125
- if (supportsAppFeatures) {
126
- const featureQuestions = []
127
-
128
- if (cliOptions.pwa === null) {
129
- featureQuestions.push({
130
- type: "confirm",
131
- name: "pwa",
132
- message: "Enable PWA support?",
133
- initial: false,
134
- })
135
- }
136
-
137
- if (cliOptions.mobile === null) {
138
- featureQuestions.push({
139
- type: "confirm",
140
- name: "mobile",
141
- message: "Add hybrid mobile support with Capacitor?",
142
- initial: false,
143
- })
144
- }
145
-
146
- if (featureQuestions.length > EMPTY_LENGTH) {
147
- const answers = await prompts(featureQuestions, { onCancel })
122
+ let features = {
123
+ ui: false,
124
+ routing: false,
125
+ icons: [],
126
+ languages: [],
127
+ mobile: false,
128
+ pwa: false,
129
+ }
148
130
 
149
- if (canceled) {
150
- console.log("Operation canceled.")
151
- return
152
- }
131
+ if (VITE_APP_TEMPLATES.has(baseTemplate)) {
132
+ features = await askAppFeatures(cliOptions)
153
133
 
154
- pwa = cliOptions.pwa === true ? true : answers.pwa === true
155
- mobile = cliOptions.mobile === true ? true : answers.mobile === true
134
+ if (!features) {
135
+ console.log("Operation canceled.")
136
+ return
156
137
  }
157
- } else if (pwa || mobile) {
138
+ } else if (cliOptions.pwa || cliOptions.mobile) {
158
139
  console.warn(
159
140
  "PWA and mobile options are only available for JavaScript and TypeScript Vite app templates.",
160
141
  )
161
- pwa = false
162
- mobile = false
163
142
  }
164
143
 
165
144
  const spinner = ora(`Creating project "${projectName}"...`).start()
@@ -194,12 +173,28 @@ async function main() {
194
173
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
195
174
  pkg.name = projectName
196
175
 
197
- if (pwa) {
176
+ const hasAppFeatures =
177
+ features.ui ||
178
+ features.routing ||
179
+ Boolean(features.icons.length) ||
180
+ Boolean(features.languages.length)
181
+
182
+ if (hasAppFeatures) {
183
+ spinner.text = "Adding app features..."
184
+ addAppFeatures(targetDir, pkg, {
185
+ ...features,
186
+ projectName,
187
+ isTypeScript: baseTemplate === "ts",
188
+ renderer,
189
+ })
190
+ }
191
+
192
+ if (features.pwa) {
198
193
  spinner.text = "Adding PWA support..."
199
194
  addPwaSupport(targetDir, pkg, projectName)
200
195
  }
201
196
 
202
- if (mobile) {
197
+ if (features.mobile) {
203
198
  spinner.text = "Adding Capacitor support..."
204
199
  addMobileSupport(targetDir, pkg, projectName)
205
200
  }
@@ -0,0 +1,280 @@
1
+ // The app's components in JSX: .jsx or .tsx files, compiled to lit-html by
2
+ // @inglorious/vite-plugin-jsx. A capitalized tag with props renders a
3
+ // component; `<Message entityId="message1" />` renders an entity. TypeScript
4
+ // expects components to be functions, so in .tsx each capitalized tag is
5
+ // preceded by a `@ts-expect-error`.
6
+
7
+ import { renderLocals, selectChange } from "./app-lit.js"
8
+ import { importBlock } from "./imports.js"
9
+
10
+ export const extension = (app) => (app.isTypeScript ? "tsx" : "jsx")
11
+
12
+ const EXPECT_ERROR =
13
+ "@ts-expect-error - Inglorious components are objects, not functions"
14
+
15
+ // The comment line before a capitalized tag in .tsx: `{/* */}` among JSX
16
+ // children, `//` inside code.
17
+ function expectError(app, indent, isChild = true) {
18
+ if (!app.isTypeScript) return ""
19
+ const comment = isChild ? `{/* ${EXPECT_ERROR} */}` : `// ${EXPECT_ERROR}`
20
+ return `${indent}${comment}\n`
21
+ }
22
+
23
+ export function appSource(app, { pages, navIcons }) {
24
+ const imports = []
25
+ if (navIcons) {
26
+ for (const icon of Object.values(navIcons)) {
27
+ imports.push(`import ${icon.name} from "${icon.from}"`)
28
+ }
29
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
30
+ }
31
+ if (app.ui) {
32
+ imports.push(
33
+ 'import { AppBar } from "@inglorious/ui/app-bar"',
34
+ 'import { Container } from "@inglorious/ui/container"',
35
+ )
36
+ if (app.tabs) {
37
+ imports.push(
38
+ 'import { BottomNavigation } from "@inglorious/ui/bottom-navigation"',
39
+ )
40
+ }
41
+ if (app.links) imports.push('import { Link } from "@inglorious/ui/link"')
42
+ if (app.i18n) imports.push('import { Select } from "@inglorious/ui/select"')
43
+ }
44
+ if (app.isTypeScript) imports.push('import type { AppApi } from "../types"')
45
+ if (app.i18n) imports.push('import { t } from "./i18n"')
46
+
47
+ const signature = app.isTypeScript
48
+ ? "render(props: Record<string, unknown> | null, api: AppApi)"
49
+ : "render(props, api)"
50
+ const content = app.routing
51
+ ? "api.render(router.route)"
52
+ : 'api.render("home")'
53
+
54
+ const shell = app.ui
55
+ ? uiShell(app, { pages, content })
56
+ : plainShell(app, { pages, content })
57
+
58
+ return `${importBlock(imports)}
59
+
60
+ export const App = {
61
+ ${signature} {
62
+ ${renderLocals(app)} return (
63
+ <>
64
+ ${shell}
65
+ </>
66
+ )
67
+ },
68
+ }
69
+ `
70
+ }
71
+
72
+ // A callback's parameter. The components' props are not typed in .tsx, so
73
+ // TypeScript cannot infer it.
74
+ function typed(app, name, type) {
75
+ return app.isTypeScript ? `${name}: ${type}` : name
76
+ }
77
+
78
+ // A page's label: translated, or the English text.
79
+ function labelText(app, page) {
80
+ return app.i18n ? `{t("${page.key}")}` : page.text
81
+ }
82
+
83
+ function labelValue(app, page) {
84
+ return app.i18n ? `t("${page.key}")` : `"${page.text}"`
85
+ }
86
+
87
+ function uiShell(app, { pages, content }) {
88
+ const nav = []
89
+ if (app.links) {
90
+ nav.push(
91
+ ...pages.map(
92
+ (page) =>
93
+ `${expectError(app, " ")} <Link href="${page.path}">${labelText(app, page)}</Link>`,
94
+ ),
95
+ )
96
+ }
97
+ if (app.i18n) {
98
+ nav.push(`${expectError(app, " ")} <Select
99
+ value={i18n.locale}
100
+ options={i18n.languages}
101
+ size="sm"
102
+ ariaLabel={t("language")}
103
+ onChange={(${typed(app, "locale", "string")}) =>
104
+ api.notify("#i18n:localeChange", locale)
105
+ }
106
+ />`)
107
+ }
108
+
109
+ const appBar = nav.length
110
+ ? `${expectError(app, " ")} <AppBar
111
+ title="${app.projectName}"
112
+ trailing={
113
+ <nav className="app-nav">
114
+ ${nav.join("\n")}
115
+ </nav>
116
+ }
117
+ />`
118
+ : `${expectError(app, " ")} <AppBar title="${app.projectName}" />`
119
+
120
+ const header = ` <header${app.mobile ? ' className="pt-safe"' : ""}>
121
+ ${appBar}
122
+ </header>
123
+ <main>
124
+ ${expectError(app, " ")} <Container maxWidth="md">{${content}}</Container>
125
+ </main>`
126
+
127
+ if (!app.tabs) return header
128
+
129
+ const actions = pages.map((page) => {
130
+ const icon = page.icon
131
+ ? `
132
+ ${expectError(app, " ", false)} icon: <Icon icon={${page.icon.name}} size="lg" />,`
133
+ : ""
134
+ return ` {
135
+ value: "${page.path}",
136
+ label: ${labelValue(app, page)},${icon}
137
+ },`
138
+ })
139
+
140
+ return `${header}
141
+ <footer className="pb-safe">
142
+ ${expectError(app, " ")} <BottomNavigation
143
+ value={router.path}
144
+ actions={[
145
+ ${actions.join("\n")}
146
+ ]}
147
+ onChange={(${typed(app, "path", "string")}) => api.notify("#router:navigate", path)}
148
+ />
149
+ </footer>`
150
+ }
151
+
152
+ function plainShell(app, { pages, content }) {
153
+ const nav = app.routing
154
+ ? pages.map(
155
+ (page) =>
156
+ ` <a href="${page.path}">${labelText(app, page)}</a>`,
157
+ )
158
+ : []
159
+ if (app.i18n) {
160
+ nav.push(` <select
161
+ aria-label={t("language")}
162
+ onChange={${selectChange(app, " ")}
163
+ }
164
+ >
165
+ {i18n.languages${app.isTypeScript ? "?." : "."}map((language) => (
166
+ <option
167
+ key={language.value}
168
+ value={language.value}
169
+ selected={language.value === i18n.locale}
170
+ >
171
+ {language.label}
172
+ </option>
173
+ ))}
174
+ </select>`)
175
+ }
176
+ const navBlock = nav.length
177
+ ? `
178
+ <nav className="app-nav">
179
+ ${nav.join("\n")}
180
+ </nav>`
181
+ : ""
182
+
183
+ return ` <header>
184
+ <strong>${app.projectName}</strong>${navBlock}
185
+ </header>
186
+ <main>{${content}}</main>`
187
+ }
188
+
189
+ export function homeSource(app, { showcase }) {
190
+ const imports = ['import { Message } from "../types/message"']
191
+ for (const icon of showcase) {
192
+ imports.push(`import ${icon.name} from "${icon.from}"`)
193
+ }
194
+ if (showcase.length) {
195
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
196
+ }
197
+ if (app.i18n) imports.push('import { t } from "../i18n"')
198
+
199
+ const text = (key, fallback) => (app.i18n ? `{t("${key}")}` : fallback)
200
+ const icons = showcase.length
201
+ ? `
202
+ <p className="app-icons">
203
+ ${showcase
204
+ .map(
205
+ (icon) =>
206
+ `${expectError(app, " ")} <Icon icon={${icon.name}} size="lg" label="${icon.label}" />`,
207
+ )
208
+ .join("\n")}
209
+ </p>`
210
+ : ""
211
+
212
+ return `${importBlock(imports)}
213
+
214
+ export const Home = {
215
+ render() {
216
+ return (
217
+ <>
218
+ <h1>${text("home.title", "Welcome")}</h1>
219
+ <p>
220
+ ${expectError(app, " ")} <Message entityId="message1" />, <Message entityId="message2" />,{" "}
221
+ ${expectError(app, " ")} <Message entityId="message3" />!
222
+ </p>
223
+ <p>${text("home.hint", "Click a greeting to shout it.")}</p>${icons}
224
+ </>
225
+ )
226
+ },
227
+ }
228
+ `
229
+ }
230
+
231
+ export function aboutSource(app) {
232
+ const head = app.i18n ? 'import { t } from "../i18n"\n\n' : ""
233
+ const text = (key, fallback) => (app.i18n ? `{t("${key}")}` : fallback)
234
+
235
+ return `${head}export const About = {
236
+ render() {
237
+ return (
238
+ <>
239
+ <h1>${text("about.title", "About")}</h1>
240
+ <p>${text("about.body", "A page of its own, at its own address.")}</p>
241
+ </>
242
+ )
243
+ },
244
+ }
245
+ `
246
+ }
247
+
248
+ export function messageSource(app) {
249
+ const imports = []
250
+ if (app.isTypeScript) {
251
+ imports.push('import type { MessageType } from "../../types"')
252
+ }
253
+ if (app.i18n) imports.push('import { t } from "../i18n"')
254
+ const greeting = app.i18n ? '{t("message.hello", { who })}' : "Hello {who}"
255
+ const declaration = app.isTypeScript
256
+ ? "export const Message: MessageType = {"
257
+ : "export const Message = {"
258
+
259
+ const span = app.i18n
260
+ ? ` <span onClick={() => api.notify(\`#\${entity.id}:click\`)}>
261
+ ${greeting}
262
+ </span>`
263
+ : ` <span onClick={() => api.notify(\`#\${entity.id}:click\`)}>${greeting}</span>`
264
+
265
+ const head = imports.length ? `${importBlock(imports)}\n\n` : ""
266
+
267
+ return `${head}${declaration}
268
+ click(entity) {
269
+ entity.isUpperCase = !entity.isUpperCase
270
+ },
271
+
272
+ render(entity, api) {
273
+ const who = entity.isUpperCase ? entity.who.toUpperCase() : entity.who
274
+ return (
275
+ ${span}
276
+ )
277
+ },
278
+ }
279
+ `
280
+ }