@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.
- package/README.md +21 -7
- package/bin/index.js +64 -88
- 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 +34 -0
- package/bin/lib/constants.js +6 -4
- package/bin/lib/features.js +171 -0
- package/bin/lib/imports.js +64 -0
- package/bin/lib/mobile.js +19 -1
- package/bin/lib/pwa.js +50 -4
- package/bin/lib/questions.js +184 -0
- package/bin/lib/source.js +58 -0
- package/package.json +4 -4
- package/templates/js/package.json +4 -4
- package/templates/js/vite.config.js +2 -1
- package/templates/js-jsx/package.json +4 -4
- package/templates/js-jsx/vite.config.js +2 -1
- package/templates/js-vue/package.json +4 -4
- 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 +8 -6
- 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 +8 -6
- package/templates/ts-jsx/src/app.tsx +2 -3
- 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 +8 -6
- package/templates/ts-vue/src/main.ts +1 -1
- 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
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
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
export function parseCliOptions(args) {
|
|
2
2
|
return {
|
|
3
|
+
projectName: args.find((arg) => !arg.startsWith("-")) ?? null,
|
|
4
|
+
template: readValueFlag(args, "template"),
|
|
5
|
+
syntax: readValueFlag(args, "syntax"),
|
|
3
6
|
mobile: readBooleanFlag(args, "mobile"),
|
|
4
7
|
pwa: readBooleanFlag(args, "pwa"),
|
|
8
|
+
ui: readBooleanFlag(args, "ui"),
|
|
9
|
+
theme: readValueFlag(args, "theme"),
|
|
10
|
+
routing: readBooleanFlag(args, "routing"),
|
|
11
|
+
transitions: readBooleanFlag(args, "transitions"),
|
|
12
|
+
icons: readListFlag(args, "icons"),
|
|
13
|
+
i18n: readBooleanFlag(args, "i18n"),
|
|
14
|
+
languages: readListFlag(args, "languages"),
|
|
5
15
|
}
|
|
6
16
|
}
|
|
7
17
|
|
|
@@ -16,3 +26,27 @@ export function readBooleanFlag(args, name) {
|
|
|
16
26
|
|
|
17
27
|
return null
|
|
18
28
|
}
|
|
29
|
+
|
|
30
|
+
// `--name=value`, or null when the flag is not given.
|
|
31
|
+
export function readValueFlag(args, name) {
|
|
32
|
+
const prefix = `--${name}=`
|
|
33
|
+
const arg = args.find((value) => value.startsWith(prefix))
|
|
34
|
+
return arg ? arg.slice(prefix.length) : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// `--name=a,b`, `--no-name` for an empty list, or null when neither is given.
|
|
38
|
+
export function readListFlag(args, name) {
|
|
39
|
+
if (args.includes(`--no-${name}`)) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = readValueFlag(args, name)
|
|
44
|
+
if (value === null) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value
|
|
49
|
+
.split(",")
|
|
50
|
+
.map((item) => item.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
}
|
package/bin/lib/constants.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export const CAPACITOR_VERSION = "^8.5.
|
|
2
|
-
export const VITE_PLUGIN_PWA_VERSION = "^
|
|
3
|
-
export const VITE_PWA_ASSETS_GENERATOR_VERSION = "^
|
|
4
|
-
|
|
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
|
+
// TypeScript 7 has no JavaScript API, which typescript-eslint and the
|
|
5
|
+
// Capacitor CLI import, so `typescript` stays on 6.
|
|
6
|
+
export const TYPESCRIPT_VERSION = "npm:@typescript/typescript6@^6.0.2"
|
|
5
7
|
export const WORKBOX_WINDOW_VERSION = "^7.4.1"
|
|
6
8
|
|
|
7
9
|
export const INDENTATION = 2
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// The choices create-app offers for lit-html apps, and what each one brings.
|
|
2
|
+
|
|
3
|
+
export const TEMPLATES = {
|
|
4
|
+
minimal: {
|
|
5
|
+
title: "Minimal",
|
|
6
|
+
description: "A single HTML file for a zero-build setup",
|
|
7
|
+
},
|
|
8
|
+
js: {
|
|
9
|
+
title: "JavaScript",
|
|
10
|
+
description: "A vanilla JavaScript setup with Vite",
|
|
11
|
+
},
|
|
12
|
+
ts: { title: "TypeScript", description: "A TypeScript setup with Vite" },
|
|
13
|
+
"ssx-js": {
|
|
14
|
+
title: "SSX (JavaScript)",
|
|
15
|
+
description: "Static Site Xecution with JavaScript",
|
|
16
|
+
},
|
|
17
|
+
"ssx-ts": {
|
|
18
|
+
title: "SSX (TypeScript)",
|
|
19
|
+
description: "Static Site Xecution with TypeScript",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SYNTAXES = {
|
|
24
|
+
"lit-html": {
|
|
25
|
+
title: "lit-html (default)",
|
|
26
|
+
description: "Tagged template literals (html`<div>...</div>`)",
|
|
27
|
+
},
|
|
28
|
+
jsx: { title: "JSX", description: "JSX/TSX syntax (<div>...</div>)" },
|
|
29
|
+
vue: { title: "Vue", description: "Vue SFC-style templates" },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const THEMES = {
|
|
33
|
+
inglorious: { title: "Inglorious", description: "Neon, videogame-inspired" },
|
|
34
|
+
material: { title: "Material", description: "Material Design inspired" },
|
|
35
|
+
bootstrap: { title: "Bootstrap", description: "Bootstrap inspired" },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Icon sets from Iconify: one module per icon, so only the icons an app
|
|
39
|
+
// imports are bundled. `nav` names the icons for the two example pages,
|
|
40
|
+
// `showcase` the one shown on the home page.
|
|
41
|
+
export const ICON_PACKS = {
|
|
42
|
+
material: {
|
|
43
|
+
title: "Material Symbols",
|
|
44
|
+
packages: { "@iconify-icons/material-symbols": "^2.0.6" },
|
|
45
|
+
nav: {
|
|
46
|
+
home: from("homeOutline", "@iconify-icons/material-symbols/home-outline"),
|
|
47
|
+
about: from(
|
|
48
|
+
"infoOutline",
|
|
49
|
+
"@iconify-icons/material-symbols/info-outline",
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
showcase: {
|
|
53
|
+
...from(
|
|
54
|
+
"favoriteOutline",
|
|
55
|
+
"@iconify-icons/material-symbols/favorite-outline",
|
|
56
|
+
),
|
|
57
|
+
label: "Material Symbols",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
fontawesome: {
|
|
61
|
+
title: "Font Awesome",
|
|
62
|
+
packages: {
|
|
63
|
+
"@iconify-icons/fa7-solid": "^2.0.0",
|
|
64
|
+
"@iconify-icons/fa7-regular": "^2.0.0",
|
|
65
|
+
"@iconify-icons/fa7-brands": "^2.0.0",
|
|
66
|
+
},
|
|
67
|
+
nav: {
|
|
68
|
+
home: from("house", "@iconify-icons/fa7-solid/house"),
|
|
69
|
+
about: from("circleInfo", "@iconify-icons/fa7-solid/circle-info"),
|
|
70
|
+
},
|
|
71
|
+
showcase: {
|
|
72
|
+
...from("fontAwesome", "@iconify-icons/fa7-brands/font-awesome"),
|
|
73
|
+
label: "Font Awesome",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
lucide: {
|
|
77
|
+
title: "Lucide",
|
|
78
|
+
packages: { "@iconify-icons/lucide": "^2.0.22" },
|
|
79
|
+
nav: {
|
|
80
|
+
home: from("house", "@iconify-icons/lucide/house"),
|
|
81
|
+
about: from("info", "@iconify-icons/lucide/info"),
|
|
82
|
+
},
|
|
83
|
+
showcase: {
|
|
84
|
+
...from("sparkles", "@iconify-icons/lucide/sparkles"),
|
|
85
|
+
label: "Lucide",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The languages offered for the example app, with its few texts in each.
|
|
91
|
+
export const LANGUAGES = {
|
|
92
|
+
en: language("English", {
|
|
93
|
+
nav: { home: "Home", about: "About" },
|
|
94
|
+
language: "Language",
|
|
95
|
+
home: { title: "Welcome", hint: "Click a greeting to shout it." },
|
|
96
|
+
about: { title: "About", body: "A page of its own, at its own address." },
|
|
97
|
+
message: { hello: "Hello {{who}}" },
|
|
98
|
+
}),
|
|
99
|
+
it: language("Italiano", {
|
|
100
|
+
nav: { home: "Home", about: "Info" },
|
|
101
|
+
language: "Lingua",
|
|
102
|
+
home: { title: "Benvenuto", hint: "Clicca un saluto per urlarlo." },
|
|
103
|
+
about: { title: "Info", body: "Una pagina tutta sua, al suo indirizzo." },
|
|
104
|
+
message: { hello: "Ciao {{who}}" },
|
|
105
|
+
}),
|
|
106
|
+
fr: language("Français", {
|
|
107
|
+
nav: { home: "Accueil", about: "À propos" },
|
|
108
|
+
language: "Langue",
|
|
109
|
+
home: { title: "Bienvenue", hint: "Cliquez sur un salut pour le crier." },
|
|
110
|
+
about: {
|
|
111
|
+
title: "À propos",
|
|
112
|
+
body: "Une page à part entière, à sa propre adresse.",
|
|
113
|
+
},
|
|
114
|
+
message: { hello: "Bonjour {{who}}" },
|
|
115
|
+
}),
|
|
116
|
+
de: language("Deutsch", {
|
|
117
|
+
nav: { home: "Start", about: "Über" },
|
|
118
|
+
language: "Sprache",
|
|
119
|
+
home: {
|
|
120
|
+
title: "Willkommen",
|
|
121
|
+
hint: "Klicke auf einen Gruß, um ihn zu rufen.",
|
|
122
|
+
},
|
|
123
|
+
about: { title: "Über", body: "Eine eigene Seite unter eigener Adresse." },
|
|
124
|
+
message: { hello: "Hallo {{who}}" },
|
|
125
|
+
}),
|
|
126
|
+
es: language("Español", {
|
|
127
|
+
nav: { home: "Inicio", about: "Acerca de" },
|
|
128
|
+
language: "Idioma",
|
|
129
|
+
home: { title: "Bienvenido", hint: "Haz clic en un saludo para gritarlo." },
|
|
130
|
+
about: {
|
|
131
|
+
title: "Acerca de",
|
|
132
|
+
body: "Una página propia, en su propia dirección.",
|
|
133
|
+
},
|
|
134
|
+
message: { hello: "Hola {{who}}" },
|
|
135
|
+
}),
|
|
136
|
+
pt: language("Português", {
|
|
137
|
+
nav: { home: "Início", about: "Sobre" },
|
|
138
|
+
language: "Idioma",
|
|
139
|
+
home: { title: "Bem-vindo", hint: "Clique numa saudação para gritá-la." },
|
|
140
|
+
about: {
|
|
141
|
+
title: "Sobre",
|
|
142
|
+
body: "Uma página própria, no seu próprio endereço.",
|
|
143
|
+
},
|
|
144
|
+
message: { hello: "Olá {{who}}" },
|
|
145
|
+
}),
|
|
146
|
+
ja: language("日本語", {
|
|
147
|
+
nav: { home: "ホーム", about: "概要" },
|
|
148
|
+
language: "言語",
|
|
149
|
+
home: {
|
|
150
|
+
title: "ようこそ",
|
|
151
|
+
hint: "あいさつをクリックすると大きくなります。",
|
|
152
|
+
},
|
|
153
|
+
about: { title: "概要", body: "専用のアドレスを持つ独立したページです。" },
|
|
154
|
+
message: { hello: "こんにちは、{{who}}" },
|
|
155
|
+
}),
|
|
156
|
+
ar: language("العربية", {
|
|
157
|
+
nav: { home: "الرئيسية", about: "حول" },
|
|
158
|
+
language: "اللغة",
|
|
159
|
+
home: { title: "مرحبًا", hint: "انقر على تحية لتكبيرها." },
|
|
160
|
+
about: { title: "حول", body: "صفحة مستقلة بعنوانها الخاص." },
|
|
161
|
+
message: { hello: "مرحبًا {{who}}" },
|
|
162
|
+
}),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function from(name, source) {
|
|
166
|
+
return { name, from: source }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function language(title, texts) {
|
|
170
|
+
return { title, texts }
|
|
171
|
+
}
|
|
@@ -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
|
+
}
|