@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,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
+ }
package/bin/lib/app.js ADDED
@@ -0,0 +1,402 @@
1
+ import fs from "fs"
2
+ import { EOL } from "os"
3
+ import path from "path"
4
+
5
+ import * as jsxComponents from "./app-jsx.js"
6
+ import * as litComponents from "./app-lit.js"
7
+ import * as vueComponents from "./app-vue.js"
8
+ import { INDENTATION } from "./constants.js"
9
+ import { ICON_PACKS, LANGUAGES } from "./features.js"
10
+ import { importBlock } from "./imports.js"
11
+ import { appModel, call, LINE_WIDTH, union } from "./source.js"
12
+
13
+ const COMPONENTS = {
14
+ "lit-html": litComponents,
15
+ jsx: jsxComponents,
16
+ vue: vueComponents,
17
+ }
18
+
19
+ /**
20
+ * Writes the source of an app with the features chosen: Inglorious UI,
21
+ * routing, page transitions, icon packs, internationalization and a mobile
22
+ * layout, in lit-html, JSX or Vue. It replaces the template's hello world,
23
+ * keeping its message type as the example on the home page. The files come
24
+ * out as Prettier and the template's ESLint config would leave them.
25
+ *
26
+ * @param {string} targetDir
27
+ * @param {object} pkg - The package.json being built; dependencies are added.
28
+ * @param {object} options
29
+ * @param {string} options.projectName
30
+ * @param {boolean} options.isTypeScript
31
+ * @param {"lit-html" | "jsx" | "vue"} options.renderer
32
+ * @param {boolean} options.ui
33
+ * @param {string} options.theme - A key of THEMES.
34
+ * @param {boolean} options.mobile
35
+ * @param {boolean} options.routing
36
+ * @param {boolean} options.transitions
37
+ * @param {string[]} options.icons - Keys of ICON_PACKS.
38
+ * @param {string[]} options.languages - Keys of LANGUAGES, the default first.
39
+ */
40
+ export function addAppFeatures(targetDir, pkg, options) {
41
+ const app = { ...options, ext: options.isTypeScript ? "ts" : "js" }
42
+ app.i18n = Boolean(app.languages.length)
43
+ // On a phone the pages are tabs at the bottom; elsewhere, links on top.
44
+ app.tabs = app.ui && app.routing && app.mobile
45
+ app.links = app.routing && !app.tabs
46
+ app.isVue = app.renderer === "vue"
47
+
48
+ const components = COMPONENTS[app.renderer]
49
+ const componentExt = components.extension(app)
50
+ const model = appModel(app)
51
+
52
+ addDependencies(pkg, app)
53
+
54
+ const files = {
55
+ [`src/main.${app.ext}`]: mainSource(app),
56
+ [`src/app.${componentExt}`]: components.appSource(app, model),
57
+ "src/app.css": appStyles(app),
58
+ [`src/pages/home.${componentExt}`]: components.homeSource(app, model),
59
+ [`src/types/message.${componentExt}`]: components.messageSource(app),
60
+ [`src/store/index.${app.ext}`]: storeSource(app),
61
+ [`src/store/entities.${app.ext}`]: entitiesSource(app),
62
+ }
63
+ if (app.routing) {
64
+ files[`src/pages/about.${componentExt}`] = components.aboutSource(app)
65
+ files[`src/store/routes.${app.ext}`] = routesSource(app)
66
+ }
67
+ if (app.i18n) {
68
+ files[`src/i18n/index.${app.ext}`] = i18nSource(app)
69
+ for (const language of app.languages) {
70
+ files[`src/i18n/locales/${language}.json`] =
71
+ JSON.stringify(LANGUAGES[language].texts, null, INDENTATION) + EOL
72
+ }
73
+ }
74
+ if (app.isTypeScript) {
75
+ files["types/index.d.ts"] = typesSource(app)
76
+ }
77
+
78
+ for (const [file, source] of Object.entries(files)) {
79
+ const filePath = path.join(targetDir, file)
80
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
81
+ fs.writeFileSync(filePath, source)
82
+ }
83
+
84
+ if (app.ui) applyTheme(targetDir, app)
85
+ }
86
+
87
+ function addDependencies(pkg, app) {
88
+ pkg.dependencies ??= {}
89
+ if (app.ui) pkg.dependencies["@inglorious/ui"] = "workspace:*"
90
+ if (app.i18n) pkg.dependencies["@inglorious/i18n"] = "workspace:*"
91
+ for (const key of app.icons) {
92
+ Object.assign(pkg.dependencies, ICON_PACKS[key].packages)
93
+ }
94
+ }
95
+
96
+ // The theme is a class on <body>, which the theme's stylesheet is scoped to.
97
+ function applyTheme(targetDir, app) {
98
+ const htmlPath = path.join(targetDir, "index.html")
99
+ if (!fs.existsSync(htmlPath)) return
100
+ const page = fs.readFileSync(htmlPath, "utf-8")
101
+ fs.writeFileSync(
102
+ htmlPath,
103
+ page.replace("<body>", `<body class="iw-theme-${app.theme}">`),
104
+ )
105
+ }
106
+
107
+ // An import of one of the app's components: Vue's name their file, and
108
+ // TypeScript cannot read .vue files, so it is told to expect that.
109
+ function componentImport(app, names, from) {
110
+ if (!app.isVue) return `import { ${names} } from "${from}"`
111
+ const line = `import { ${names} } from "${from}.vue"`
112
+ return app.isTypeScript
113
+ ? `// @ts-expect-error - Handled by @inglorious/vite-plugin-vue\n${line}`
114
+ : line
115
+ }
116
+
117
+ // ---------------------------------------------------------------- main
118
+
119
+ function mainSource(app) {
120
+ const imports = []
121
+ if (app.ui) {
122
+ imports.push(
123
+ 'import "@inglorious/ui/base.css"',
124
+ `import "@inglorious/ui/themes/${app.theme}.css"`,
125
+ ...uiComponents(app).map((name) => `import "@inglorious/ui/${name}.css"`),
126
+ )
127
+ if (app.transitions) {
128
+ imports.push('import "@inglorious/ui/view-transitions.css"')
129
+ }
130
+ }
131
+ imports.push('import "./app.css"')
132
+ if (app.transitions) {
133
+ imports.push(
134
+ 'import { withTransitions } from "@inglorious/ui/view-transitions"',
135
+ )
136
+ }
137
+ imports.push(
138
+ 'import { mount } from "@inglorious/web"',
139
+ componentImport(app, "App", "./app"),
140
+ 'import { store } from "./store"',
141
+ )
142
+ if (app.i18n) imports.push('import { ready } from "./i18n"')
143
+
144
+ const args = [
145
+ app.transitions ? "withTransitions(store)" : "store",
146
+ "(api) => App.render(null, api)",
147
+ app.isTypeScript
148
+ ? 'document.getElementById("root")!'
149
+ : 'document.getElementById("root")',
150
+ ]
151
+
152
+ const body = app.i18n
153
+ ? `// Mounts once the starting language has loaded, so the first render is
154
+ // already translated.
155
+ ready.then(() =>
156
+ ${call("mount", args, " ", ",")}
157
+ )`
158
+ : call("mount", args, "", "")
159
+
160
+ return `${importBlock(imports)}
161
+
162
+ ${body}
163
+ `
164
+ }
165
+
166
+ // The Inglorious UI components the app uses, for their stylesheets.
167
+ function uiComponents(app) {
168
+ const names = ["app-bar", "container"]
169
+ if (app.links) names.push("link")
170
+ if (app.tabs) names.push("bottom-navigation")
171
+ if (app.icons.length) names.push("icon")
172
+ if (app.i18n) names.push("select")
173
+ return names
174
+ }
175
+
176
+ function appStyles(app) {
177
+ const layout = `body {
178
+ min-height: 100dvh;
179
+ display: flex;
180
+ flex-direction: column;
181
+ }
182
+
183
+ #root {
184
+ flex: 1;
185
+ display: flex;
186
+ flex-direction: column;
187
+ }
188
+
189
+ main {
190
+ flex: 1;
191
+ padding-block: 1.5rem;
192
+ }
193
+
194
+ .app-nav {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 1rem;
198
+ }
199
+ `
200
+ if (app.ui) {
201
+ // In Vue the navigation is the app bar's content rather than its trailing
202
+ // slot, so it pushes itself to the end.
203
+ return app.isVue
204
+ ? `${layout}
205
+ .iw-app-bar .app-nav {
206
+ margin-inline-start: auto;
207
+ }
208
+ `
209
+ : layout
210
+ }
211
+ return `${layout}
212
+ header {
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: space-between;
216
+ padding: 1rem 1.5rem;
217
+ border-block-end: 1px solid #ddd;
218
+ }
219
+
220
+ main {
221
+ padding-inline: 1.5rem;
222
+ }
223
+ `
224
+ }
225
+
226
+ // ---------------------------------------------------------------- store
227
+
228
+ function storeSource(app) {
229
+ const imports = [
230
+ app.isTypeScript
231
+ ? 'import { createStore, type TypesConfig } from "@inglorious/store"'
232
+ : 'import { createStore } from "@inglorious/store"',
233
+ componentImport(app, "Home", "../pages/home"),
234
+ componentImport(app, "Message", "../types/message"),
235
+ 'import { entities } from "./entities"',
236
+ 'import { middlewares } from "./middlewares"',
237
+ ]
238
+ if (app.routing) {
239
+ imports.push(
240
+ 'import { Router, setRoutes } from "@inglorious/web/router"',
241
+ componentImport(app, "About", "../pages/about"),
242
+ 'import { routes } from "./routes"',
243
+ )
244
+ }
245
+ if (app.isTypeScript) {
246
+ imports.push('import type { AppEntity, AppState } from "../../types"')
247
+ }
248
+ if (app.i18n) imports.push('import { I18n } from "../i18n"')
249
+
250
+ const types = ["Message", "Home"]
251
+ if (app.routing) types.push("About", "Router")
252
+ if (app.i18n) types.push("I18n")
253
+ const cast = app.isTypeScript ? " as unknown as TypesConfig<AppEntity>" : ""
254
+ const oneLine = ` types: { ${types.join(", ")} }${cast},`
255
+ const typesValue =
256
+ oneLine.length <= LINE_WIDTH
257
+ ? oneLine
258
+ : ` types: {
259
+ ${types.map((type) => ` ${type},`).join("\n")}
260
+ }${cast},`
261
+ const generics = app.isTypeScript ? "<AppEntity, AppState>" : ""
262
+
263
+ return `${importBlock(imports)}
264
+ ${app.routing ? "\nsetRoutes(routes)\n" : ""}
265
+ export const store = createStore${generics}({
266
+ ${typesValue}
267
+ entities,
268
+ middlewares,
269
+ // Creates an entity for each type that has none: ${singletons(app).join(", ")}.
270
+ autoCreateEntities: true,
271
+ })
272
+ `
273
+ }
274
+
275
+ // The entities the store creates on its own, as the comment in the store
276
+ // lists them.
277
+ function singletons(app) {
278
+ const ids = ["home"]
279
+ if (app.routing) ids.push("about", "router")
280
+ if (app.i18n) ids.push("i18n")
281
+ return ids
282
+ }
283
+
284
+ // Only the entities that need data, or come more than one to a type; the
285
+ // store creates the rest.
286
+ function entitiesSource(app) {
287
+ const head = app.isTypeScript
288
+ ? `import type { AppState } from "../../types"
289
+
290
+ export const entities: Partial<AppState> = {`
291
+ : "export const entities = {"
292
+ return `${head}
293
+ message1: { type: "Message", who: "world" },
294
+ message2: { type: "Message", who: "wide" },
295
+ message3: { type: "Message", who: "web" },
296
+ }
297
+ `
298
+ }
299
+
300
+ function routesSource(app) {
301
+ const head = app.isTypeScript
302
+ ? `import type { RoutesConfig } from "@inglorious/web/router"
303
+
304
+ // Each path names the entity its page renders.
305
+ export const routes: RoutesConfig = {`
306
+ : `// Each path names the entity its page renders.
307
+ export const routes = {`
308
+ return `${head}
309
+ "/": "home",
310
+ "/about": "about",
311
+ }
312
+ `
313
+ }
314
+
315
+ // ---------------------------------------------------------------- i18n
316
+
317
+ function i18nSource(app) {
318
+ const [defaultLocale] = app.languages
319
+ return `import { createI18n } from "@inglorious/i18n"
320
+
321
+ // One catalogue per language in ./locales, loaded when it is needed. Adding a
322
+ // language is adding its file.
323
+ export const { I18n, t, ready } = createI18n({
324
+ catalogues: import.meta.glob("./locales/*.json"),
325
+ defaultLocale: "${defaultLocale}",
326
+ storageKey: "${app.projectName}-locale",
327
+ })
328
+ `
329
+ }
330
+
331
+ // ---------------------------------------------------------------- types
332
+
333
+ function typesSource(app) {
334
+ const imports = [
335
+ 'import "./vite-end"',
336
+ 'import type { BaseEntity, EntitiesState } from "@inglorious/store"',
337
+ 'import type { Api, TemplateResult } from "@inglorious/web"',
338
+ ]
339
+ const entities = ["MessageEntity", "HomeEntity"]
340
+ const declarations = [
341
+ `export interface MessageEntity extends BaseEntity {
342
+ type: "Message"
343
+ id?: string
344
+ who: string
345
+ isUpperCase?: boolean
346
+ }`,
347
+ `export interface HomeEntity extends BaseEntity {
348
+ type: "Home"
349
+ }`,
350
+ ]
351
+ const state = [
352
+ " home: HomeEntity",
353
+ " message1: MessageEntity",
354
+ " message2: MessageEntity",
355
+ " message3: MessageEntity",
356
+ ]
357
+
358
+ if (app.routing) {
359
+ imports.push('import type { RouterEntity } from "@inglorious/web/router"')
360
+ entities.push("AboutEntity", "AppRouterEntity")
361
+ declarations.push(
362
+ `export interface AboutEntity extends BaseEntity {
363
+ type: "About"
364
+ }`,
365
+ `export interface AppRouterEntity extends BaseEntity, Omit<RouterEntity, "id"> {
366
+ type: "Router"
367
+ }`,
368
+ )
369
+ state.unshift(" router: AppRouterEntity")
370
+ state.push(" about: AboutEntity")
371
+ }
372
+ if (app.i18n) {
373
+ imports.push('import type { I18nEntity } from "@inglorious/i18n"')
374
+ entities.push("AppI18nEntity")
375
+ declarations.push(
376
+ `export interface AppI18nEntity extends BaseEntity, Omit<I18nEntity, "id"> {
377
+ type: "I18n"
378
+ }`,
379
+ )
380
+ state.unshift(" i18n: AppI18nEntity")
381
+ }
382
+
383
+ return `${importBlock(imports)}
384
+
385
+ ${declarations.join("\n\n")}
386
+
387
+ // Union type of all entities
388
+ ${union("AppEntity", entities)}
389
+
390
+ // State type with known entity IDs
391
+ export interface AppState extends EntitiesState<AppEntity> {
392
+ ${state.join("\n")}
393
+ }
394
+
395
+ export type AppApi = Api<AppEntity, AppState>
396
+
397
+ export interface MessageType {
398
+ click: (entity: MessageEntity) => void
399
+ render: (entity: MessageEntity, api: AppApi) => TemplateResult | null
400
+ }
401
+ `
402
+ }
package/bin/lib/cli.js ADDED
@@ -0,0 +1,49 @@
1
+ export function parseCliOptions(args) {
2
+ return {
3
+ mobile: readBooleanFlag(args, "mobile"),
4
+ pwa: readBooleanFlag(args, "pwa"),
5
+ ui: readBooleanFlag(args, "ui"),
6
+ theme: readValueFlag(args, "theme"),
7
+ routing: readBooleanFlag(args, "routing"),
8
+ transitions: readBooleanFlag(args, "transitions"),
9
+ icons: readListFlag(args, "icons"),
10
+ i18n: readBooleanFlag(args, "i18n"),
11
+ languages: readListFlag(args, "languages"),
12
+ }
13
+ }
14
+
15
+ export function readBooleanFlag(args, name) {
16
+ if (args.includes(`--${name}`)) {
17
+ return true
18
+ }
19
+
20
+ if (args.includes(`--no-${name}`)) {
21
+ return false
22
+ }
23
+
24
+ return null
25
+ }
26
+
27
+ // `--name=value`, or null when the flag is not given.
28
+ export function readValueFlag(args, name) {
29
+ const prefix = `--${name}=`
30
+ const arg = args.find((value) => value.startsWith(prefix))
31
+ return arg ? arg.slice(prefix.length) : null
32
+ }
33
+
34
+ // `--name=a,b`, `--no-name` for an empty list, or null when neither is given.
35
+ export function readListFlag(args, name) {
36
+ if (args.includes(`--no-${name}`)) {
37
+ return []
38
+ }
39
+
40
+ const value = readValueFlag(args, name)
41
+ if (value === null) {
42
+ return null
43
+ }
44
+
45
+ return value
46
+ .split(",")
47
+ .map((item) => item.trim())
48
+ .filter(Boolean)
49
+ }
@@ -0,0 +1,14 @@
1
+ export const CAPACITOR_VERSION = "^8.5.3"
2
+ export const VITE_PLUGIN_PWA_VERSION = "^2.0.0"
3
+ export const VITE_PWA_ASSETS_GENERATOR_VERSION = "^2.0.0"
4
+ export const TYPESCRIPT_VERSION = "^7.0.2"
5
+ export const WORKBOX_WINDOW_VERSION = "^7.4.1"
6
+
7
+ export const INDENTATION = 2
8
+ export const AFTER_FIRST_CHARACTER_INDEX = 1
9
+ export const EMPTY_LENGTH = 0
10
+ export const FIRST_CHARACTER_INDEX = 0
11
+ export const FIRST_ITEM_INDEX = 0
12
+ export const NPM_VIEW_TIMEOUT_MS = 10_000
13
+ export const USER_ARG_START_INDEX = 2
14
+ export const VITE_APP_TEMPLATES = new Set(["js", "ts"])