@inglorious/create-app 1.1.2 → 1.3.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 +21 -7
  2. package/bin/index.js +64 -88
  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 +34 -0
  8. package/bin/lib/constants.js +6 -4
  9. package/bin/lib/features.js +171 -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 +4 -4
  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 +8 -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 +8 -6
  33. package/templates/ts-jsx/src/app.tsx +2 -3
  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 +8 -6
  41. package/templates/ts-vue/src/main.ts +1 -1
  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,275 @@
1
+ // The app's components in lit-html: tagged templates in .js or .ts files.
2
+
3
+ import { importBlock } from "./imports.js"
4
+
5
+ export const extension = (app) => (app.isTypeScript ? "ts" : "js")
6
+
7
+ export function appSource(app, { pages, navIcons }) {
8
+ const imports = ['import { html } from "@inglorious/web"']
9
+ if (navIcons) {
10
+ for (const icon of Object.values(navIcons)) {
11
+ imports.push(`import ${icon.name} from "${icon.from}"`)
12
+ }
13
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
14
+ }
15
+ if (app.ui) {
16
+ imports.push(
17
+ 'import { AppBar } from "@inglorious/ui/app-bar"',
18
+ 'import { Container } from "@inglorious/ui/container"',
19
+ )
20
+ if (app.tabs) {
21
+ imports.push(
22
+ 'import { BottomNavigation } from "@inglorious/ui/bottom-navigation"',
23
+ )
24
+ }
25
+ if (app.links) imports.push('import { Link } from "@inglorious/ui/link"')
26
+ if (app.i18n) imports.push('import { Select } from "@inglorious/ui/select"')
27
+ }
28
+ if (app.isTypeScript) imports.push('import type { AppApi } from "../types"')
29
+ if (app.i18n) imports.push('import { t } from "./i18n"')
30
+
31
+ const label = (key, text) => (app.i18n ? `t("${key}")` : `"${text}"`)
32
+ const signature = app.isTypeScript
33
+ ? "render(props: Record<string, unknown> | null, api: AppApi)"
34
+ : "render(props, api)"
35
+
36
+ const content = app.routing
37
+ ? "api.render(router.route)"
38
+ : 'api.render("home")'
39
+
40
+ const template = app.ui
41
+ ? uiShell(app, { label, pages, content })
42
+ : plainShell(app, { label, pages, content })
43
+
44
+ return `${importBlock(imports)}
45
+
46
+ export const App = {
47
+ ${signature} {
48
+ ${renderLocals(app)} return html\`
49
+ ${template}
50
+ \`
51
+ },
52
+ }
53
+ `
54
+ }
55
+
56
+ // The app's own entities are always there; TypeScript cannot know that.
57
+ export function renderLocals(app) {
58
+ const always = app.isTypeScript ? "!" : ""
59
+ const locals = []
60
+ if (app.routing) {
61
+ locals.push(` const router = api.getEntity("router")${always}`)
62
+ }
63
+ if (app.i18n) locals.push(` const i18n = api.getEntity("i18n")${always}`)
64
+ return locals.length ? `${locals.join("\n")}\n\n` : ""
65
+ }
66
+
67
+ function uiShell(app, { label, pages, content }) {
68
+ const nav = []
69
+ if (app.links) {
70
+ nav.push(
71
+ ...pages.map(
72
+ (page) =>
73
+ `Link.render({ href: "${page.path}", children: ${label(page.key, page.text)} })`,
74
+ ),
75
+ )
76
+ }
77
+ if (app.i18n) {
78
+ nav.push(`Select.render({
79
+ value: i18n.locale,
80
+ options: i18n.languages,
81
+ size: "sm",
82
+ ariaLabel: t("language"),
83
+ onChange: (locale) => api.notify("#i18n:localeChange", locale),
84
+ })`)
85
+ }
86
+
87
+ const trailing = nav.length
88
+ ? `
89
+ trailing: html\`<nav class="app-nav">
90
+ \${[
91
+ ${nav.join(",\n ")},
92
+ ]}
93
+ </nav>\`,`
94
+ : ""
95
+
96
+ const header = ` <header${app.mobile ? ' class="pt-safe"' : ""}>
97
+ \${AppBar.render({
98
+ title: "${app.projectName}",${trailing}
99
+ })}
100
+ </header>
101
+ <main>
102
+ \${Container.render({ maxWidth: "md", children: ${content} })}
103
+ </main>`
104
+
105
+ if (!app.tabs) return header
106
+
107
+ const actions = pages.map(
108
+ (page) => ` {
109
+ value: "${page.path}",
110
+ label: ${label(page.key, page.text)},${
111
+ page.icon
112
+ ? `
113
+ icon: Icon.render({ icon: ${page.icon.name}, size: "lg" }),`
114
+ : ""
115
+ }
116
+ },`,
117
+ )
118
+
119
+ return `${header}
120
+ <footer class="pb-safe">
121
+ \${BottomNavigation.render({
122
+ value: router.path,
123
+ actions: [
124
+ ${actions.join("\n")}
125
+ ],
126
+ onChange: (path) => api.notify("#router:navigate", path),
127
+ })}
128
+ </footer>`
129
+ }
130
+
131
+ // The native language picker's change handler, laid out as Prettier would at
132
+ // `indent`. TypeScript has to be told what the event's target is.
133
+ export function selectChange(app, indent) {
134
+ if (!app.isTypeScript) {
135
+ return `(event) =>
136
+ ${indent} api.notify("#i18n:localeChange", event.target.value)`
137
+ }
138
+ return `(event: Event) =>
139
+ ${indent} api.notify(
140
+ ${indent} "#i18n:localeChange",
141
+ ${indent} (event.target as HTMLSelectElement).value,
142
+ ${indent} )`
143
+ }
144
+
145
+ function plainShell(app, { label, pages, content }) {
146
+ const nav = app.routing
147
+ ? pages.map(
148
+ (page) =>
149
+ ` <a href="${page.path}">\${${label(page.key, page.text)}}</a>`,
150
+ )
151
+ : []
152
+ if (app.i18n) {
153
+ nav.push(` <select
154
+ aria-label=\${t("language")}
155
+ @change=\${${selectChange(app, " ")}}
156
+ >
157
+ \${i18n.languages${app.isTypeScript ? "?." : "."}map(
158
+ (language) =>
159
+ html\`<option
160
+ value=\${language.value}
161
+ ?selected=\${language.value === i18n.locale}
162
+ >
163
+ \${language.label}
164
+ </option>\`,
165
+ )}
166
+ </select>`)
167
+ }
168
+ const navBlock = nav.length
169
+ ? `
170
+ <nav class="app-nav">
171
+ ${nav.join("\n")}
172
+ </nav>`
173
+ : ""
174
+
175
+ return ` <header>
176
+ <strong>${app.projectName}</strong>${navBlock}
177
+ </header>
178
+ <main>\${${content}}</main>`
179
+ }
180
+
181
+ export function homeSource(app, { showcase }) {
182
+ const imports = ['import { html } from "@inglorious/web"']
183
+ for (const icon of showcase) {
184
+ imports.push(`import ${icon.name} from "${icon.from}"`)
185
+ }
186
+ if (showcase.length) {
187
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
188
+ }
189
+ if (app.isTypeScript) {
190
+ imports.push('import type { AppApi, HomeEntity } from "../../types"')
191
+ }
192
+ if (app.i18n) imports.push('import { t } from "../i18n"')
193
+
194
+ const text = (key, fallback) => (app.i18n ? `\${t("${key}")}` : fallback)
195
+ const icons = showcase.length
196
+ ? `
197
+ <p class="app-icons">
198
+ ${showcase
199
+ .map(
200
+ (icon) => ` \${Icon.render({
201
+ icon: ${icon.name},
202
+ size: "lg",
203
+ label: "${icon.label}",
204
+ })}`,
205
+ )
206
+ .join("\n")}
207
+ </p>`
208
+ : ""
209
+
210
+ const signature = app.isTypeScript
211
+ ? "render(entity: HomeEntity, api: AppApi)"
212
+ : "render(entity, api)"
213
+
214
+ return `${importBlock(imports)}
215
+
216
+ export const Home = {
217
+ ${signature} {
218
+ return html\`
219
+ <h1>${text("home.title", "Welcome")}</h1>
220
+ <p>
221
+ \${api.render("message1")}, \${api.render("message2")},
222
+ \${api.render("message3")}!
223
+ </p>
224
+ <p>${text("home.hint", "Click a greeting to shout it.")}</p>${icons}
225
+ \`
226
+ },
227
+ }
228
+ `
229
+ }
230
+
231
+ export function aboutSource(app) {
232
+ const imports = ['import { html } from "@inglorious/web"']
233
+ if (app.i18n) imports.push('import { t } from "../i18n"')
234
+ const text = (key, fallback) => (app.i18n ? `\${t("${key}")}` : fallback)
235
+
236
+ return `${importBlock(imports)}
237
+
238
+ export const About = {
239
+ render() {
240
+ return html\`
241
+ <h1>${text("about.title", "About")}</h1>
242
+ <p>${text("about.body", "A page of its own, at its own address.")}</p>
243
+ \`
244
+ },
245
+ }
246
+ `
247
+ }
248
+
249
+ export function messageSource(app) {
250
+ const imports = ['import { html } from "@inglorious/web"']
251
+ if (app.isTypeScript) {
252
+ imports.push('import type { MessageType } from "../../types"')
253
+ }
254
+ if (app.i18n) imports.push('import { t } from "../i18n"')
255
+ const greeting = app.i18n ? '${t("message.hello", { who })}' : "Hello ${who}"
256
+ const declaration = app.isTypeScript
257
+ ? "export const Message: MessageType = {"
258
+ : "export const Message = {"
259
+
260
+ return `${importBlock(imports)}
261
+
262
+ ${declaration}
263
+ click(entity) {
264
+ entity.isUpperCase = !entity.isUpperCase
265
+ },
266
+
267
+ render(entity, api) {
268
+ const who = entity.isUpperCase ? entity.who.toUpperCase() : entity.who
269
+ return html\`<span @click=\${() => api.notify(\`#\${entity.id}:click\`)}
270
+ >${greeting}</span
271
+ >\`
272
+ },
273
+ }
274
+ `
275
+ }
@@ -0,0 +1,239 @@
1
+ // The app's components as Vue single-file components, compiled to lit-html by
2
+ // @inglorious/vite-plugin-vue. Imported Inglorious UI components are tags with
3
+ // props; imported .vue components with `v-bind="id"` render store entities;
4
+ // script values that read `api` are computed on every render.
5
+
6
+ import { importBlock } from "./imports.js"
7
+ import { LINE_WIDTH } from "./source.js"
8
+
9
+ export const extension = () => "vue"
10
+
11
+ export function appSource(app, { pages, navIcons }) {
12
+ const imports = []
13
+ if (navIcons) {
14
+ for (const icon of Object.values(navIcons)) {
15
+ imports.push(`import ${icon.name} from "${icon.from}"`)
16
+ }
17
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
18
+ }
19
+ if (app.ui) {
20
+ imports.push(
21
+ 'import { AppBar } from "@inglorious/ui/app-bar"',
22
+ 'import { Container } from "@inglorious/ui/container"',
23
+ )
24
+ if (app.tabs) {
25
+ imports.push(
26
+ 'import { BottomNavigation } from "@inglorious/ui/bottom-navigation"',
27
+ )
28
+ }
29
+ if (app.links) imports.push('import { Link } from "@inglorious/ui/link"')
30
+ if (app.i18n) imports.push('import { Select } from "@inglorious/ui/select"')
31
+ }
32
+ if (app.i18n) imports.push('import { t } from "./i18n"')
33
+
34
+ const locals = []
35
+ if (app.routing) locals.push('const router = api.getEntity("router")')
36
+ if (app.i18n) locals.push('const i18n = api.getEntity("i18n")')
37
+
38
+ const content = app.routing
39
+ ? "api.render(router.route)"
40
+ : 'api.render("home")'
41
+ const label = (key, text) => (app.i18n ? `{{ t("${key}") }}` : text)
42
+ const quotedLabel = (key, text) => (app.i18n ? `t('${key}')` : `'${text}'`)
43
+
44
+ const template = app.ui
45
+ ? uiShell(app, { label, quotedLabel, pages, content })
46
+ : plainShell(app, { label, pages, content })
47
+
48
+ return `${script(imports, locals)}<template>
49
+ ${template}
50
+ </template>
51
+ `
52
+ }
53
+
54
+ function script(imports, locals) {
55
+ if (!imports.length && !locals.length) return ""
56
+ const parts = []
57
+ if (imports.length) parts.push(importBlock(imports))
58
+ if (locals.length) parts.push(locals.join("\n"))
59
+ return `<script>
60
+ ${parts.join("\n\n")}
61
+ </script>
62
+
63
+ `
64
+ }
65
+
66
+ function uiShell(app, { label, quotedLabel, pages, content }) {
67
+ const nav = []
68
+ if (app.links) {
69
+ nav.push(
70
+ ...pages.map(
71
+ (page) =>
72
+ ` <Link href="${page.path}">${label(page.key, page.text)}</Link>`,
73
+ ),
74
+ )
75
+ }
76
+ if (app.i18n) {
77
+ nav.push(` <Select
78
+ :value="i18n.locale"
79
+ :options="i18n.languages"
80
+ size="sm"
81
+ :ariaLabel="t('language')"
82
+ :onChange="(locale) => api.notify('#i18n:localeChange', locale)"
83
+ />`)
84
+ }
85
+ const navBlock = nav.length
86
+ ? `
87
+ <nav class="app-nav">
88
+ ${nav.join("\n")}
89
+ </nav>
90
+ `
91
+ : ""
92
+ const appBar = nav.length
93
+ ? `<AppBar title="${app.projectName}">${navBlock}</AppBar>`
94
+ : `<AppBar title="${app.projectName}" />`
95
+
96
+ const header = ` <header${app.mobile ? ' class="pt-safe"' : ""}>
97
+ ${appBar}
98
+ </header>
99
+ <main>
100
+ <Container maxWidth="md">{{ ${content} }}</Container>
101
+ </main>`
102
+
103
+ if (!app.tabs) return header
104
+
105
+ // Each action on one line when it fits, one property per line otherwise.
106
+ const actions = pages.map((page) => {
107
+ const fields = [
108
+ `value: '${page.path}'`,
109
+ `label: ${quotedLabel(page.key, page.text)}`,
110
+ ]
111
+ if (page.icon) {
112
+ fields.push(`icon: Icon.render({ icon: ${page.icon.name}, size: 'lg' })`)
113
+ }
114
+ const line = ` { ${fields.join(", ")} },`
115
+ if (line.length <= LINE_WIDTH) return line
116
+ return ` {
117
+ ${fields.map((field) => ` ${field},`).join("\n")}
118
+ },`
119
+ })
120
+
121
+ return `${header}
122
+ <footer class="pb-safe">
123
+ <BottomNavigation
124
+ :value="router.path"
125
+ :actions="[
126
+ ${actions.join("\n")}
127
+ ]"
128
+ :onChange="(path) => api.notify('#router:navigate', path)"
129
+ />
130
+ </footer>`
131
+ }
132
+
133
+ function plainShell(app, { label, pages, content }) {
134
+ const nav = app.routing
135
+ ? pages.map(
136
+ (page) =>
137
+ ` <a href="${page.path}">${label(page.key, page.text)}</a>`,
138
+ )
139
+ : []
140
+ if (app.i18n) {
141
+ nav.push(` <select
142
+ :aria-label="t('language')"
143
+ @change="
144
+ (event) => api.notify('#i18n:localeChange', event.target.value)
145
+ "
146
+ >
147
+ <option
148
+ v-for="language in i18n.languages"
149
+ :key="language.value"
150
+ :value="language.value"
151
+ :selected="language.value === i18n.locale"
152
+ >
153
+ {{ language.label }}
154
+ </option>
155
+ </select>`)
156
+ }
157
+ const navBlock = nav.length
158
+ ? `
159
+ <nav class="app-nav">
160
+ ${nav.join("\n")}
161
+ </nav>`
162
+ : ""
163
+
164
+ return ` <header>
165
+ <strong>${app.projectName}</strong>${navBlock}
166
+ </header>
167
+ <main>{{ ${content} }}</main>`
168
+ }
169
+
170
+ export function homeSource(app, { showcase }) {
171
+ const imports = ['import { Message } from "../types/message.vue"']
172
+ for (const icon of showcase) {
173
+ imports.push(`import ${icon.name} from "${icon.from}"`)
174
+ }
175
+ if (showcase.length) {
176
+ imports.push('import { Icon } from "@inglorious/ui/icon"')
177
+ }
178
+ if (app.i18n) imports.push('import { t } from "../i18n"')
179
+
180
+ const text = (key, fallback) => (app.i18n ? `{{ t("${key}") }}` : fallback)
181
+ const icons = showcase.length
182
+ ? `
183
+ <p class="app-icons">
184
+ ${showcase
185
+ .map(
186
+ (icon) =>
187
+ ` <Icon :icon="${icon.name}" size="lg" label="${icon.label}" />`,
188
+ )
189
+ .join("\n")}
190
+ </p>`
191
+ : ""
192
+
193
+ return `${script(imports, [])}<template>
194
+ <h1>${text("home.title", "Welcome")}</h1>
195
+ <p>
196
+ <Message v-bind="message1" />, <Message v-bind="message2" />,
197
+ <Message v-bind="message3" />!
198
+ </p>
199
+ <p>${text("home.hint", "Click a greeting to shout it.")}</p>${icons}
200
+ </template>
201
+ `
202
+ }
203
+
204
+ export function aboutSource(app) {
205
+ const imports = app.i18n ? ['import { t } from "../i18n"'] : []
206
+ const text = (key, fallback) => (app.i18n ? `{{ t("${key}") }}` : fallback)
207
+ return `${script(imports, [])}<template>
208
+ <h1>${text("about.title", "About")}</h1>
209
+ <p>${text("about.body", "A page of its own, at its own address.")}</p>
210
+ </template>
211
+ `
212
+ }
213
+
214
+ export function messageSource(app) {
215
+ const imports = app.i18n ? ['import { t } from "../i18n"'] : []
216
+ const who = "entity.isUpperCase ? entity.who.toUpperCase() : entity.who"
217
+ const span = app.i18n
218
+ ? ` <span @click="click">{{
219
+ t("message.hello", {
220
+ who: ${who},
221
+ })
222
+ }}</span>`
223
+ : ` <span @click="click"
224
+ >Hello
225
+ {{ ${who} }}</span
226
+ >`
227
+ const scriptBody = [
228
+ ...(imports.length ? [importBlock(imports), ""] : []),
229
+ "const click = (entity) => (entity.isUpperCase = !entity.isUpperCase)",
230
+ ]
231
+ return `<script>
232
+ ${scriptBody.join("\n")}
233
+ </script>
234
+
235
+ <template>
236
+ ${span}
237
+ </template>
238
+ `
239
+ }