@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,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"])
|