@inglorious/create-app 1.10.0 → 1.12.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 +2 -2
- package/bin/index.js +5 -0
- package/bin/lib/app.js +134 -18
- package/bin/lib/constants.js +1 -0
- package/bin/lib/features.js +59 -0
- package/bin/lib/file.js +32 -0
- package/bin/lib/firebase.js +2 -16
- package/bin/lib/native-update.js +119 -0
- package/bin/lib/skills.js +1 -0
- package/bin/lib/source.js +1 -0
- package/package.json +1 -1
- package/templates/features/native-update/supabase/migrations/native_update.sql +17 -0
- package/templates/ssx-ts/types/index.d.ts +10 -2
- package/templates/ssx-ts-jsx/types/index.d.ts +10 -2
- package/templates/ts/src/app/index.ts +1 -1
- package/templates/ts/src/main.ts +6 -1
- package/templates/ts/src/store/index.ts +2 -2
- package/templates/ts/types/index.d.ts +10 -2
- package/templates/ts-jsx/src/main.ts +1 -1
- package/templates/ts-jsx/src/store/index.ts +2 -2
- package/templates/ts-jsx/types/index.d.ts +10 -2
- package/templates/ts-vue/src/main.ts +6 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ For the JavaScript and TypeScript Vite templates, the CLI then asks about the ap
|
|
|
31
31
|
| Add push notifications? | `--push` / `--no-push` | Firebase Cloud Messaging, a permission prompt and the Supabase side of sending; turns on PWA support |
|
|
32
32
|
| Enable PWA support? | `--pwa` / `--no-pwa` | `vite-plugin-pwa`, a manifest, icons, and a service worker that keeps installed apps on the latest version |
|
|
33
33
|
|
|
34
|
-
Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, `i18n`, `firebase` and `supabase` when the project uses them. If the install fails, the CLI prints the command to retry it.
|
|
34
|
+
Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `app-recipe`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, `i18n`, `firebase` and `supabase` when the project uses them. If the install fails, the CLI prints the command to retry it.
|
|
35
35
|
|
|
36
36
|
The app is generated in the syntax you chose: lit-html, JSX or Vue. Every question answered on the command line is skipped, so a project can be created without any prompt:
|
|
37
37
|
|
|
@@ -99,7 +99,7 @@ For Android and iOS builds, install Android Studio with the Android SDK and a Ja
|
|
|
99
99
|
|
|
100
100
|
Every Vite app is versioned with [Changesets](https://github.com/changesets/changesets): describe each change with `pnpm changeset`, and `pnpm release:version` raises the version in `package.json` and writes `CHANGELOG.md`. The app reads that version as `import.meta.env.VITE_APP_VERSION`.
|
|
101
101
|
|
|
102
|
-
Mobile apps also get `pnpm mobile:release`, which copies the version into the iOS and Android projects, raises the build number they share (every store upload needs a new one), builds with the values in `.env` rather than `.env.local`, and syncs. The generated README describes the whole release, upload included.
|
|
102
|
+
Mobile apps also get `pnpm mobile:release`, which copies the version into the iOS and Android projects, raises the build number they share (every store upload needs a new one), builds with the values in `.env` rather than `.env.local`, and syncs. The generated README describes the whole release, upload included. With `--push` too, an `app_versions` table in Supabase holds each platform's minimum build: an older install shows nothing but an update page, so raising it makes a release mandatory.
|
|
103
103
|
|
|
104
104
|
## License
|
|
105
105
|
|
package/bin/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { SYNTAXES, TEMPLATES } from "./lib/features.js"
|
|
|
20
20
|
import { copyDir } from "./lib/file.js"
|
|
21
21
|
import { addFirebaseSupport } from "./lib/firebase.js"
|
|
22
22
|
import { addMobileSupport } from "./lib/mobile.js"
|
|
23
|
+
import { addNativeUpdateSupport } from "./lib/native-update.js"
|
|
23
24
|
import { addPwaSupport } from "./lib/pwa.js"
|
|
24
25
|
import { askAppFeatures } from "./lib/questions.js"
|
|
25
26
|
import { addReleaseSupport } from "./lib/release.js"
|
|
@@ -207,6 +208,10 @@ async function main() {
|
|
|
207
208
|
addFirebaseSupport(targetDir, pkg, { ...features, templatesRoot })
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
if (features.mobile && features.push) {
|
|
212
|
+
addNativeUpdateSupport(targetDir, templatesRoot)
|
|
213
|
+
}
|
|
214
|
+
|
|
210
215
|
for (const depType of ["dependencies", "devDependencies"]) {
|
|
211
216
|
if (pkg[depType]) {
|
|
212
217
|
for (const [name, version] of Object.entries(pkg[depType])) {
|
package/bin/lib/app.js
CHANGED
|
@@ -6,14 +6,21 @@ import * as jsxComponents from "./app-jsx.js"
|
|
|
6
6
|
import * as litComponents from "./app-lit.js"
|
|
7
7
|
import * as vueComponents from "./app-vue.js"
|
|
8
8
|
import {
|
|
9
|
+
CAPACITOR_APP_VERSION,
|
|
9
10
|
CAPACITOR_FIREBASE_VERSION,
|
|
10
11
|
CAPACITOR_VERSION,
|
|
11
12
|
FIREBASE_VERSION,
|
|
12
13
|
INDENTATION,
|
|
13
14
|
SUPABASE_VERSION,
|
|
14
15
|
} from "./constants.js"
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
ICON_PACKS,
|
|
18
|
+
LANGUAGES,
|
|
19
|
+
NATIVE_UPDATE_TEXTS,
|
|
20
|
+
PROMPT_TEXTS,
|
|
21
|
+
} from "./features.js"
|
|
16
22
|
import { importBlock } from "./imports.js"
|
|
23
|
+
import { nativeUpdateFiles } from "./native-update.js"
|
|
17
24
|
import { envExample, promptFiles } from "./prompts.js"
|
|
18
25
|
import {
|
|
19
26
|
appModel,
|
|
@@ -33,7 +40,8 @@ const COMPONENTS = {
|
|
|
33
40
|
/**
|
|
34
41
|
* Writes the source of an app with the features chosen: Inglorious UI,
|
|
35
42
|
* routing, page transitions, icon packs, internationalization, a mobile
|
|
36
|
-
* layout, analytics and push prompts,
|
|
43
|
+
* layout, analytics and push prompts, and the page that blocks outdated native
|
|
44
|
+
* builds, in lit-html, JSX or Vue. It replaces the template's hello world,
|
|
37
45
|
* keeping its message type as the example on the home page. The files come
|
|
38
46
|
* out as Prettier and the template's ESLint config would leave them.
|
|
39
47
|
*
|
|
@@ -60,6 +68,7 @@ export function addAppFeatures(targetDir, pkg, options) {
|
|
|
60
68
|
app.links = app.routing && !app.tabs
|
|
61
69
|
app.isVue = app.renderer === "vue"
|
|
62
70
|
app.hasPrompts = app.analytics || app.push
|
|
71
|
+
app.nativeUpdate = app.mobile && app.push
|
|
63
72
|
app.layout = layout(app)
|
|
64
73
|
|
|
65
74
|
const components = COMPONENTS[app.renderer]
|
|
@@ -93,6 +102,7 @@ export function addAppFeatures(targetDir, pkg, options) {
|
|
|
93
102
|
Object.assign(files, promptFiles(app))
|
|
94
103
|
files[".env.example"] = envExample(app)
|
|
95
104
|
}
|
|
105
|
+
if (app.nativeUpdate) Object.assign(files, nativeUpdateFiles(app))
|
|
96
106
|
if (app.isTypeScript) {
|
|
97
107
|
files["types/index.d.ts"] = typesSource(app)
|
|
98
108
|
}
|
|
@@ -140,6 +150,10 @@ function addDependencies(pkg, app) {
|
|
|
140
150
|
CAPACITOR_FIREBASE_VERSION
|
|
141
151
|
pkg.dependencies["@supabase/supabase-js"] = SUPABASE_VERSION
|
|
142
152
|
}
|
|
153
|
+
if (app.nativeUpdate) {
|
|
154
|
+
pkg.dependencies["@capacitor/app"] = CAPACITOR_APP_VERSION
|
|
155
|
+
pkg.dependencies["@inglorious/supabase"] = "workspace:*"
|
|
156
|
+
}
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
function catalogue(app, language) {
|
|
@@ -148,6 +162,7 @@ function catalogue(app, language) {
|
|
|
148
162
|
...LANGUAGES[language].texts,
|
|
149
163
|
...(app.analytics && { analytics }),
|
|
150
164
|
...(app.push && { push }),
|
|
165
|
+
...(app.nativeUpdate && { nativeUpdate: NATIVE_UPDATE_TEXTS[language] }),
|
|
151
166
|
}
|
|
152
167
|
}
|
|
153
168
|
|
|
@@ -195,12 +210,22 @@ function mainSource(app) {
|
|
|
195
210
|
if (app.hasPrompts) {
|
|
196
211
|
imports.push('import { renderPrompts } from "./common/prompt"')
|
|
197
212
|
}
|
|
213
|
+
if (app.nativeUpdate) {
|
|
214
|
+
imports.push(
|
|
215
|
+
'import { App as NativeApp } from "@capacitor/app"',
|
|
216
|
+
'import { when } from "@inglorious/web/directives/when"',
|
|
217
|
+
'import { watchNativeUpdate } from "@inglorious/web/native-update"',
|
|
218
|
+
)
|
|
219
|
+
if (app.isTypeScript) imports.push('import type { AppApi } from "../types"')
|
|
220
|
+
}
|
|
198
221
|
|
|
199
222
|
const args = [
|
|
200
223
|
"store",
|
|
201
|
-
app.
|
|
202
|
-
? "
|
|
203
|
-
:
|
|
224
|
+
app.nativeUpdate
|
|
225
|
+
? "renderApp"
|
|
226
|
+
: app.hasPrompts
|
|
227
|
+
? "(api) => html`${App.render(null, api)} ${renderPrompts(api)}`"
|
|
228
|
+
: "(api) => App.render(null, api)",
|
|
204
229
|
app.isTypeScript
|
|
205
230
|
? 'document.getElementById("root")!'
|
|
206
231
|
: 'document.getElementById("root")',
|
|
@@ -209,9 +234,7 @@ function mainSource(app) {
|
|
|
209
234
|
const body = app.i18n
|
|
210
235
|
? `// Mounts once the starting language has loaded, so the first render is
|
|
211
236
|
// already translated.
|
|
212
|
-
|
|
213
|
-
${call("mount", args, " ", ",")}
|
|
214
|
-
)`
|
|
237
|
+
${afterReady(args)}`
|
|
215
238
|
: call("mount", args, "", "")
|
|
216
239
|
|
|
217
240
|
const session = app.push
|
|
@@ -222,12 +245,35 @@ supabase.auth.onAuthStateChange((_event, session) => {
|
|
|
222
245
|
})`
|
|
223
246
|
: ""
|
|
224
247
|
|
|
248
|
+
const nativeUpdate = app.nativeUpdate
|
|
249
|
+
? `
|
|
250
|
+
|
|
251
|
+
watchNativeUpdate(NativeApp, store)
|
|
252
|
+
|
|
253
|
+
// An outdated native build shows nothing but the update page.
|
|
254
|
+
function renderApp(api${app.isTypeScript ? ": AppApi" : ""}) {
|
|
255
|
+
return html\`\${when(
|
|
256
|
+
api.getEntity("nativeUpdate")${app.isTypeScript ? "!" : ""}.isUpdateRequired,
|
|
257
|
+
() => api.render("nativeUpdate"),
|
|
258
|
+
() => html\`\${App.render(null, api)} \${renderPrompts(api)}\`,
|
|
259
|
+
)}\`
|
|
260
|
+
}`
|
|
261
|
+
: ""
|
|
262
|
+
|
|
225
263
|
return `${importBlock(imports)}
|
|
226
264
|
|
|
227
|
-
${body}${session}
|
|
265
|
+
${body}${session}${nativeUpdate}
|
|
228
266
|
`
|
|
229
267
|
}
|
|
230
268
|
|
|
269
|
+
function afterReady(args) {
|
|
270
|
+
const oneLine = `ready.then(() => ${call("mount", args, "", "")})`
|
|
271
|
+
if (!oneLine.includes("\n") && oneLine.length <= LINE_WIDTH) return oneLine
|
|
272
|
+
return `ready.then(() =>
|
|
273
|
+
${call("mount", args, " ", ",")}
|
|
274
|
+
)`
|
|
275
|
+
}
|
|
276
|
+
|
|
231
277
|
function uiComponents(app) {
|
|
232
278
|
const names = ["app-bar", "container"]
|
|
233
279
|
if (app.links) names.push("link")
|
|
@@ -244,6 +290,18 @@ body {
|
|
|
244
290
|
}
|
|
245
291
|
`
|
|
246
292
|
|
|
293
|
+
const UPDATE_PAGE = `
|
|
294
|
+
.app-update {
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
align-items: center;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
gap: 1rem;
|
|
300
|
+
padding: 1.5rem;
|
|
301
|
+
text-align: center;
|
|
302
|
+
}
|
|
303
|
+
`
|
|
304
|
+
|
|
247
305
|
const PLAIN_PROMPT = `
|
|
248
306
|
.app-prompt {
|
|
249
307
|
position: fixed;
|
|
@@ -287,7 +345,7 @@ main {
|
|
|
287
345
|
align-items: center;
|
|
288
346
|
gap: 1rem;
|
|
289
347
|
}
|
|
290
|
-
${app.tabs && app.hasPrompts ? PROMPT_ABOVE_TABS : ""}`
|
|
348
|
+
${app.tabs && app.hasPrompts ? PROMPT_ABOVE_TABS : ""}${app.nativeUpdate ? UPDATE_PAGE : ""}`
|
|
291
349
|
if (app.ui) {
|
|
292
350
|
return app.isVue
|
|
293
351
|
? `${layout}
|
|
@@ -333,7 +391,9 @@ function storeSource(app) {
|
|
|
333
391
|
)
|
|
334
392
|
}
|
|
335
393
|
if (app.isTypeScript) {
|
|
336
|
-
imports.push(
|
|
394
|
+
imports.push(
|
|
395
|
+
'import type { AppEntity, AppEvents, AppState } from "../../types"',
|
|
396
|
+
)
|
|
337
397
|
}
|
|
338
398
|
if (app.i18n) imports.push('import { I18n } from "../i18n"')
|
|
339
399
|
if (app.analytics) {
|
|
@@ -346,12 +406,18 @@ function storeSource(app) {
|
|
|
346
406
|
`import { PushPrompt } from "${relative("src/store", app.layout.pushPrompt)}"`,
|
|
347
407
|
)
|
|
348
408
|
}
|
|
409
|
+
if (app.nativeUpdate) {
|
|
410
|
+
imports.push(
|
|
411
|
+
`import { NativeUpdate } from "${relative("src/store", app.layout.nativeUpdate)}"`,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
349
414
|
|
|
350
415
|
const types = ["Message", "Home"]
|
|
351
416
|
if (app.routing) types.push("About", "Router")
|
|
352
417
|
if (app.i18n) types.push("I18n")
|
|
353
418
|
if (app.analytics) types.push("Analytics")
|
|
354
419
|
if (app.push) types.push("PushPrompt")
|
|
420
|
+
if (app.nativeUpdate) types.push("NativeUpdate")
|
|
355
421
|
const oneLine = ` types: { ${types.join(", ")} },`
|
|
356
422
|
const typesValue =
|
|
357
423
|
oneLine.length <= LINE_WIDTH
|
|
@@ -359,7 +425,7 @@ function storeSource(app) {
|
|
|
359
425
|
: ` types: {
|
|
360
426
|
${types.map((type) => ` ${type},`).join("\n")}
|
|
361
427
|
},`
|
|
362
|
-
const generics = app.isTypeScript ? "<AppEntity, AppState>" : ""
|
|
428
|
+
const generics = app.isTypeScript ? "<AppEntity, AppState, AppEvents>" : ""
|
|
363
429
|
|
|
364
430
|
return `${importBlock(imports)}
|
|
365
431
|
${app.routing ? "\nsetRoutes(routes)\n" : ""}
|
|
@@ -379,6 +445,7 @@ function singletons(app) {
|
|
|
379
445
|
if (app.i18n) ids.push("i18n")
|
|
380
446
|
if (app.analytics) ids.push("analytics")
|
|
381
447
|
if (app.push) ids.push("pushPrompt")
|
|
448
|
+
if (app.nativeUpdate) ids.push("nativeUpdate")
|
|
382
449
|
return ids
|
|
383
450
|
}
|
|
384
451
|
|
|
@@ -425,13 +492,35 @@ export const { I18n, t, ready } = createI18n({
|
|
|
425
492
|
`
|
|
426
493
|
}
|
|
427
494
|
|
|
495
|
+
/**
|
|
496
|
+
* `export type AppEvents = EventsOf<...>`, wrapped as Prettier would: on one
|
|
497
|
+
* line if it fits, then with the types on a line of their own, then one per
|
|
498
|
+
* line.
|
|
499
|
+
*/
|
|
500
|
+
function eventsOf(types) {
|
|
501
|
+
const head = "export type AppEvents = EventsOf<"
|
|
502
|
+
const oneLine = `${head}${types.join(" | ")}>`
|
|
503
|
+
if (oneLine.length <= LINE_WIDTH) return oneLine
|
|
504
|
+
const inner = ` ${types.join(" | ")}`
|
|
505
|
+
if (inner.length <= LINE_WIDTH) return `${head}\n${inner}\n>`
|
|
506
|
+
return `${head}\n${types.map((type) => ` | ${type}`).join("\n")}\n>`
|
|
507
|
+
}
|
|
508
|
+
|
|
428
509
|
function typesSource(app) {
|
|
429
510
|
const imports = [
|
|
430
511
|
'import "./vite-env"',
|
|
431
|
-
|
|
512
|
+
`import type {
|
|
513
|
+
BaseEntity,
|
|
514
|
+
EntitiesState,
|
|
515
|
+
EntityType,
|
|
516
|
+
EventsOf,
|
|
517
|
+
} from "@inglorious/store"`,
|
|
432
518
|
'import type { Api, TemplateResult } from "@inglorious/web"',
|
|
433
519
|
]
|
|
434
520
|
const entities = ["MessageEntity", "HomeEntity"]
|
|
521
|
+
// The types whose events the app sends: its own, then the libraries'.
|
|
522
|
+
const eventTypes = ["MessageType"]
|
|
523
|
+
if (app.renderer === "lit-html") eventTypes.push("HomeType")
|
|
435
524
|
const declarations = [
|
|
436
525
|
`export interface MessageEntity extends BaseEntity {
|
|
437
526
|
type: "Message"
|
|
@@ -450,7 +539,10 @@ function typesSource(app) {
|
|
|
450
539
|
]
|
|
451
540
|
|
|
452
541
|
if (app.routing) {
|
|
453
|
-
imports.push(
|
|
542
|
+
imports.push(
|
|
543
|
+
'import type { RouterEntity, RouterType } from "@inglorious/web/router"',
|
|
544
|
+
)
|
|
545
|
+
eventTypes.push("RouterType")
|
|
454
546
|
entities.push("AboutEntity", "AppRouterEntity")
|
|
455
547
|
declarations.push(
|
|
456
548
|
`export interface AboutEntity extends BaseEntity {
|
|
@@ -464,7 +556,8 @@ function typesSource(app) {
|
|
|
464
556
|
state.push(" about: AboutEntity")
|
|
465
557
|
}
|
|
466
558
|
if (app.i18n) {
|
|
467
|
-
imports.push('import type { I18nEntity } from "@inglorious/i18n"')
|
|
559
|
+
imports.push('import type { I18nEntity, I18nType } from "@inglorious/i18n"')
|
|
560
|
+
eventTypes.push("I18nType")
|
|
468
561
|
entities.push("AppI18nEntity")
|
|
469
562
|
declarations.push(
|
|
470
563
|
`export interface AppI18nEntity extends I18nEntity {
|
|
@@ -475,8 +568,9 @@ function typesSource(app) {
|
|
|
475
568
|
}
|
|
476
569
|
if (app.analytics) {
|
|
477
570
|
imports.push(
|
|
478
|
-
'import type { AnalyticsEntity } from "@inglorious/web/analytics"',
|
|
571
|
+
'import type { AnalyticsEntity, AnalyticsType } from "@inglorious/web/analytics"',
|
|
479
572
|
)
|
|
573
|
+
eventTypes.push("AnalyticsType")
|
|
480
574
|
entities.push("AppAnalyticsEntity")
|
|
481
575
|
declarations.push(
|
|
482
576
|
`export interface AppAnalyticsEntity extends AnalyticsEntity {
|
|
@@ -486,7 +580,10 @@ function typesSource(app) {
|
|
|
486
580
|
state.push(" analytics: AppAnalyticsEntity")
|
|
487
581
|
}
|
|
488
582
|
if (app.push) {
|
|
489
|
-
imports.push(
|
|
583
|
+
imports.push(
|
|
584
|
+
'import type { PushPromptEntity, PushPromptType } from "@inglorious/web/push"',
|
|
585
|
+
)
|
|
586
|
+
eventTypes.push("PushPromptType")
|
|
490
587
|
entities.push("AppPushPromptEntity")
|
|
491
588
|
declarations.push(
|
|
492
589
|
`export interface AppPushPromptEntity extends PushPromptEntity {
|
|
@@ -495,6 +592,22 @@ function typesSource(app) {
|
|
|
495
592
|
)
|
|
496
593
|
state.push(" pushPrompt: AppPushPromptEntity")
|
|
497
594
|
}
|
|
595
|
+
if (app.nativeUpdate) {
|
|
596
|
+
imports.push(
|
|
597
|
+
`import type {
|
|
598
|
+
NativeUpdateEntity,
|
|
599
|
+
NativeUpdateType,
|
|
600
|
+
} from "@inglorious/web/native-update"`,
|
|
601
|
+
)
|
|
602
|
+
eventTypes.push("NativeUpdateType")
|
|
603
|
+
entities.push("AppNativeUpdateEntity")
|
|
604
|
+
declarations.push(
|
|
605
|
+
`export interface AppNativeUpdateEntity extends NativeUpdateEntity {
|
|
606
|
+
type: "NativeUpdate"
|
|
607
|
+
}`,
|
|
608
|
+
)
|
|
609
|
+
state.push(" nativeUpdate: AppNativeUpdateEntity")
|
|
610
|
+
}
|
|
498
611
|
|
|
499
612
|
return `${importBlock(imports)}
|
|
500
613
|
|
|
@@ -508,7 +621,10 @@ export interface AppState extends EntitiesState<AppEntity> {
|
|
|
508
621
|
${state.join("\n")}
|
|
509
622
|
}
|
|
510
623
|
|
|
511
|
-
|
|
624
|
+
// The events the app's types answer, so that \`notify\` checks them.
|
|
625
|
+
${eventsOf(eventTypes)}
|
|
626
|
+
|
|
627
|
+
export type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
512
628
|
|
|
513
629
|
// Each type's events and their payloads. Declare types with \`type\`, not
|
|
514
630
|
// \`interface\`: an interface does not fit the store's \`types\`.
|
package/bin/lib/constants.js
CHANGED
package/bin/lib/features.js
CHANGED
|
@@ -272,3 +272,62 @@ function from(name, source) {
|
|
|
272
272
|
function language(title, texts) {
|
|
273
273
|
return { title, texts }
|
|
274
274
|
}
|
|
275
|
+
|
|
276
|
+
export const NATIVE_UPDATE_TEXTS = {
|
|
277
|
+
en: {
|
|
278
|
+
title: "Time to update",
|
|
279
|
+
body: "This version of the app is no longer supported. Update it to keep going.",
|
|
280
|
+
bodyNoLink:
|
|
281
|
+
"This version of the app is no longer supported. Update it from your app store to keep going.",
|
|
282
|
+
update: "Update",
|
|
283
|
+
},
|
|
284
|
+
it: {
|
|
285
|
+
title: "È ora di aggiornare",
|
|
286
|
+
body: "Questa versione dell'app non è più supportata. Aggiornala per continuare.",
|
|
287
|
+
bodyNoLink:
|
|
288
|
+
"Questa versione dell'app non è più supportata. Aggiornala dal tuo store per continuare.",
|
|
289
|
+
update: "Aggiorna",
|
|
290
|
+
},
|
|
291
|
+
fr: {
|
|
292
|
+
title: "Mise à jour nécessaire",
|
|
293
|
+
body: "Cette version de l'app n'est plus prise en charge. Mettez-la à jour pour continuer.",
|
|
294
|
+
bodyNoLink:
|
|
295
|
+
"Cette version de l'app n'est plus prise en charge. Mettez-la à jour depuis votre store pour continuer.",
|
|
296
|
+
update: "Mettre à jour",
|
|
297
|
+
},
|
|
298
|
+
de: {
|
|
299
|
+
title: "Zeit für ein Update",
|
|
300
|
+
body: "Diese Version der App wird nicht mehr unterstützt. Aktualisiere sie, um weiterzumachen.",
|
|
301
|
+
bodyNoLink:
|
|
302
|
+
"Diese Version der App wird nicht mehr unterstützt. Aktualisiere sie in deinem App Store, um weiterzumachen.",
|
|
303
|
+
update: "Aktualisieren",
|
|
304
|
+
},
|
|
305
|
+
es: {
|
|
306
|
+
title: "Hora de actualizar",
|
|
307
|
+
body: "Esta versión de la app ya no es compatible. Actualízala para continuar.",
|
|
308
|
+
bodyNoLink:
|
|
309
|
+
"Esta versión de la app ya no es compatible. Actualízala desde tu tienda de apps para continuar.",
|
|
310
|
+
update: "Actualizar",
|
|
311
|
+
},
|
|
312
|
+
pt: {
|
|
313
|
+
title: "Hora de atualizar",
|
|
314
|
+
body: "Esta versão da app já não é suportada. Atualiza-a para continuar.",
|
|
315
|
+
bodyNoLink:
|
|
316
|
+
"Esta versão da app já não é suportada. Atualiza-a na tua loja de apps para continuar.",
|
|
317
|
+
update: "Atualizar",
|
|
318
|
+
},
|
|
319
|
+
ja: {
|
|
320
|
+
title: "アップデートが必要です",
|
|
321
|
+
body: "このバージョンのアプリはサポートが終了しました。引き続き使うにはアップデートしてください。",
|
|
322
|
+
bodyNoLink:
|
|
323
|
+
"このバージョンのアプリはサポートが終了しました。引き続き使うにはアプリストアからアップデートしてください。",
|
|
324
|
+
update: "アップデート",
|
|
325
|
+
},
|
|
326
|
+
ar: {
|
|
327
|
+
title: "حان وقت التحديث",
|
|
328
|
+
body: "لم يعد هذا الإصدار من التطبيق مدعومًا. حدّثه للمتابعة.",
|
|
329
|
+
bodyNoLink:
|
|
330
|
+
"لم يعد هذا الإصدار من التطبيق مدعومًا. حدّثه من متجر التطبيقات للمتابعة.",
|
|
331
|
+
update: "تحديث",
|
|
332
|
+
},
|
|
333
|
+
}
|
package/bin/lib/file.js
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
FIRST_ITEM_INDEX,
|
|
9
9
|
} from "./constants.js"
|
|
10
10
|
|
|
11
|
+
const MIGRATION_STAMP_LENGTH = 14
|
|
12
|
+
const ONE_SECOND = 1
|
|
13
|
+
|
|
11
14
|
export function copyDir(src, dest) {
|
|
12
15
|
fs.mkdirSync(dest, { recursive: true })
|
|
13
16
|
|
|
@@ -64,6 +67,28 @@ export function injectAfterImport(source, importSource, newImport) {
|
|
|
64
67
|
)
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Prefixes a migration with the current time, as `supabase migration new`
|
|
72
|
+
* does, a second later than any migration already there.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} targetDir
|
|
75
|
+
* @param {string} name - The file in supabase/migrations.
|
|
76
|
+
*/
|
|
77
|
+
export function stampMigration(targetDir, name) {
|
|
78
|
+
const migrations = path.join(targetDir, "supabase/migrations")
|
|
79
|
+
const taken = new Set(
|
|
80
|
+
fs.readdirSync(migrations).map((file) => file.split("_")[FIRST_ITEM_INDEX]),
|
|
81
|
+
)
|
|
82
|
+
const time = new Date()
|
|
83
|
+
while (taken.has(migrationStamp(time))) {
|
|
84
|
+
time.setSeconds(time.getSeconds() + ONE_SECOND)
|
|
85
|
+
}
|
|
86
|
+
fs.renameSync(
|
|
87
|
+
path.join(migrations, name),
|
|
88
|
+
path.join(migrations, `${migrationStamp(time)}_${name}`),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
export function escapeRegExp(value) {
|
|
68
93
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
69
94
|
}
|
|
@@ -86,3 +111,10 @@ export function toJavaIdentifier(value) {
|
|
|
86
111
|
|
|
87
112
|
return identifier || "app"
|
|
88
113
|
}
|
|
114
|
+
|
|
115
|
+
function migrationStamp(time) {
|
|
116
|
+
return time
|
|
117
|
+
.toISOString()
|
|
118
|
+
.replace(/\D/g, "")
|
|
119
|
+
.slice(FIRST_ITEM_INDEX, MIGRATION_STAMP_LENGTH)
|
|
120
|
+
}
|
package/bin/lib/firebase.js
CHANGED
|
@@ -2,10 +2,8 @@ import fs from "fs"
|
|
|
2
2
|
import { EOL } from "os"
|
|
3
3
|
import path from "path"
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { copyDir } from "./file.js"
|
|
5
|
+
import { copyDir, stampMigration } from "./file.js"
|
|
7
6
|
|
|
8
|
-
const MIGRATION_STAMP_LENGTH = 14
|
|
9
7
|
const FIRST_NUMBER = 1
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -27,7 +25,7 @@ export function addFirebaseSupport(targetDir, pkg, options) {
|
|
|
27
25
|
|
|
28
26
|
if (push) {
|
|
29
27
|
copyDir(path.join(templatesRoot, "features/push"), targetDir)
|
|
30
|
-
stampMigration(targetDir)
|
|
28
|
+
stampMigration(targetDir, "push.sql")
|
|
31
29
|
pkg.scripts["vault:push"] = "node scripts/push-vault-secrets.mjs"
|
|
32
30
|
keepSupabaseOutOfTypeScript(targetDir)
|
|
33
31
|
}
|
|
@@ -63,18 +61,6 @@ function replaceInFile(file, search, replacement) {
|
|
|
63
61
|
)
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
function stampMigration(targetDir) {
|
|
67
|
-
const migrations = path.join(targetDir, "supabase/migrations")
|
|
68
|
-
const stamp = new Date()
|
|
69
|
-
.toISOString()
|
|
70
|
-
.replace(/\D/g, "")
|
|
71
|
-
.slice(FIRST_ITEM_INDEX, MIGRATION_STAMP_LENGTH)
|
|
72
|
-
fs.renameSync(
|
|
73
|
-
path.join(migrations, "push.sql"),
|
|
74
|
-
path.join(migrations, `${stamp}_push.sql`),
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
64
|
function appendSetupToReadme(targetDir, { analytics, push, mobile }) {
|
|
79
65
|
const readmePath = path.join(targetDir, "README.md")
|
|
80
66
|
if (!fs.existsSync(readmePath)) return
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import { EOL } from "os"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
import { copyDir, stampMigration } from "./file.js"
|
|
6
|
+
import { importBlock } from "./imports.js"
|
|
7
|
+
import { relative } from "./source.js"
|
|
8
|
+
|
|
9
|
+
const README_SECTION = `## Mandatory Native Updates
|
|
10
|
+
|
|
11
|
+
The native apps carry their web code inside the binary, so only the stores update them. Each platform's row in the \`app_versions\` table holds the oldest build allowed to run: an older one shows nothing but an update page, at launch and whenever it comes back to the foreground.
|
|
12
|
+
|
|
13
|
+
Once a new build is live in a store, raise that platform's minimum to its build number:
|
|
14
|
+
|
|
15
|
+
\`\`\`sql
|
|
16
|
+
update public.app_versions set min_build = 7 where platform = 'android';
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
Never before: users would be blocked with nothing to download. Set \`store_url\` to the store page, or the update page shows no button. A failed check keeps the last answer, so nobody is blocked for being offline.
|
|
20
|
+
|
|
21
|
+
Older builds keep talking to the backend until they are blocked, so a change that breaks them ships in two releases: a build that works with both backends, made mandatory, then the breaking change.`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The source of the NativeUpdate type: the page that blocks a native build
|
|
25
|
+
* older than the minimum in Supabase. Lit-html in every syntax, like the
|
|
26
|
+
* prompts: src/main renders it in place of the app.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ ui: boolean, i18n: boolean, ext: string, isTypeScript: boolean, layout: Record<string, string> }} app
|
|
29
|
+
* @returns {Record<string, string>} Sources by file path.
|
|
30
|
+
*/
|
|
31
|
+
export function nativeUpdateFiles(app) {
|
|
32
|
+
return {
|
|
33
|
+
[`${app.layout.nativeUpdate}/index.${app.ext}`]: nativeUpdateSource(app),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adds what mandatory native updates need outside src/: the app_versions
|
|
39
|
+
* table and how to raise its minimum in the README.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} targetDir
|
|
42
|
+
* @param {string} templatesRoot
|
|
43
|
+
*/
|
|
44
|
+
export function addNativeUpdateSupport(targetDir, templatesRoot) {
|
|
45
|
+
copyDir(path.join(templatesRoot, "features/native-update"), targetDir)
|
|
46
|
+
stampMigration(targetDir, "native_update.sql")
|
|
47
|
+
appendToReadme(targetDir)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nativeUpdateSource(app) {
|
|
51
|
+
const at = app.layout.nativeUpdate
|
|
52
|
+
const imports = [
|
|
53
|
+
'import { supabaseNativeUpdate } from "@inglorious/supabase"',
|
|
54
|
+
'import { html } from "@inglorious/web"',
|
|
55
|
+
'import { when } from "@inglorious/web/directives/when"',
|
|
56
|
+
'import { createNativeUpdate } from "@inglorious/web/native-update"',
|
|
57
|
+
`import { supabase } from "${relative(at, "src/api/supabase")}"`,
|
|
58
|
+
]
|
|
59
|
+
if (app.ui) imports.push('import { Button } from "@inglorious/ui/button"')
|
|
60
|
+
if (app.i18n) imports.push(`import { t } from "${relative(at, "src/i18n")}"`)
|
|
61
|
+
if (app.isTypeScript) {
|
|
62
|
+
imports.push(
|
|
63
|
+
`import type { AppApi, AppNativeUpdateEntity } from "${relative(at, "types")}"`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
const label = (key, text) =>
|
|
67
|
+
app.i18n ? `t("nativeUpdate.${key}")` : JSON.stringify(text)
|
|
68
|
+
const text = (key, plain) =>
|
|
69
|
+
app.i18n ? `\${t("nativeUpdate.${key}")}` : plain
|
|
70
|
+
const params = app.isTypeScript
|
|
71
|
+
? "entity: AppNativeUpdateEntity, api: AppApi"
|
|
72
|
+
: "entity, api"
|
|
73
|
+
const button = app.ui
|
|
74
|
+
? `\${when(entity.storeUrl, () =>
|
|
75
|
+
Button.render({
|
|
76
|
+
children: ${label("update", "Update")},
|
|
77
|
+
color: "primary",
|
|
78
|
+
onClick: () => api.notify(\`#\${entity.id}:nativeUpdateApply\`),
|
|
79
|
+
}),
|
|
80
|
+
)}`
|
|
81
|
+
: `\${when(
|
|
82
|
+
entity.storeUrl,
|
|
83
|
+
() =>
|
|
84
|
+
html\`<button
|
|
85
|
+
@click=\${() => api.notify(\`#\${entity.id}:nativeUpdateApply\`)}
|
|
86
|
+
>
|
|
87
|
+
${text("update", "Update")}
|
|
88
|
+
</button>\`,
|
|
89
|
+
)}`
|
|
90
|
+
|
|
91
|
+
return `${importBlock(imports)}
|
|
92
|
+
|
|
93
|
+
export const NativeUpdate = {
|
|
94
|
+
...createNativeUpdate({ adapter: supabaseNativeUpdate(supabase) }),
|
|
95
|
+
|
|
96
|
+
render(${params}) {
|
|
97
|
+
const body = entity.storeUrl
|
|
98
|
+
? ${label("body", "This version of the app is no longer supported. Update it to keep going.")}
|
|
99
|
+
: ${label("bodyNoLink", "This version of the app is no longer supported. Update it from your app store to keep going.")}
|
|
100
|
+
|
|
101
|
+
return html\`<main class="app-update">
|
|
102
|
+
<h1>${text("title", "Time to update")}</h1>
|
|
103
|
+
<p>\${body}</p>
|
|
104
|
+
${button}
|
|
105
|
+
</main>\`
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function appendToReadme(targetDir) {
|
|
112
|
+
const readmePath = path.join(targetDir, "README.md")
|
|
113
|
+
if (!fs.existsSync(readmePath)) return
|
|
114
|
+
const readme = fs.readFileSync(readmePath, "utf-8")
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
readmePath,
|
|
117
|
+
`${readme.trimEnd()}${EOL}${EOL}${README_SECTION}${EOL}`,
|
|
118
|
+
)
|
|
119
|
+
}
|
package/bin/lib/skills.js
CHANGED
package/bin/lib/source.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/create-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "A scaffolding tool to quickly create a new app with Inglorious Web.",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- The oldest native build allowed to run on each platform. Readable before
|
|
2
|
+
-- sign-in, so an outdated app is stopped on its login screen too.
|
|
3
|
+
create table public.app_versions (
|
|
4
|
+
platform text primary key check (platform in ('ios', 'android')),
|
|
5
|
+
min_build integer not null check (min_build >= 1),
|
|
6
|
+
store_url text
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
alter table public.app_versions enable row level security;
|
|
10
|
+
|
|
11
|
+
create policy "Anyone reads the minimum app versions"
|
|
12
|
+
on public.app_versions for select to anon, authenticated
|
|
13
|
+
using (true);
|
|
14
|
+
|
|
15
|
+
insert into public.app_versions (platform, min_build) values
|
|
16
|
+
('ios', 1),
|
|
17
|
+
('android', 1);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BaseEntity,
|
|
3
|
+
EntitiesState,
|
|
4
|
+
EntityType,
|
|
5
|
+
EventsOf,
|
|
6
|
+
} from "@inglorious/store"
|
|
2
7
|
import type { Api, TemplateResult } from "@inglorious/web"
|
|
3
8
|
|
|
4
9
|
export interface AboutEntity extends BaseEntity {
|
|
@@ -12,7 +17,10 @@ export interface AppState extends EntitiesState<AppEntity> {
|
|
|
12
17
|
about: AboutEntity
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
// The events the types below answer, so that `notify` checks them.
|
|
21
|
+
export type AppEvents = EventsOf<AboutType>
|
|
22
|
+
|
|
23
|
+
export type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
16
24
|
|
|
17
25
|
export type AboutType = {
|
|
18
26
|
click: (entity: AboutEntity) => void
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/// <reference types="@inglorious/jsx/jsx-runtime" />
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BaseEntity,
|
|
5
|
+
EntitiesState,
|
|
6
|
+
EntityType,
|
|
7
|
+
EventsOf,
|
|
8
|
+
} from "@inglorious/store"
|
|
4
9
|
import type { Api, TemplateResult } from "@inglorious/web"
|
|
5
10
|
|
|
6
11
|
export interface AboutEntity extends BaseEntity {
|
|
@@ -14,7 +19,10 @@ export interface AppState extends EntitiesState<AppEntity> {
|
|
|
14
19
|
about: AboutEntity
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
// The events the types below answer, so that `notify` checks them.
|
|
23
|
+
export type AppEvents = EventsOf<AboutType>
|
|
24
|
+
|
|
25
|
+
export type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
18
26
|
|
|
19
27
|
export type AboutType = {
|
|
20
28
|
click: (entity: AboutEntity) => void
|
|
@@ -3,7 +3,7 @@ import { html } from "@inglorious/web"
|
|
|
3
3
|
import type { AppApi } from "../../types"
|
|
4
4
|
|
|
5
5
|
export const App = {
|
|
6
|
-
render(
|
|
6
|
+
render(_props: Record<string, unknown> | null, api: AppApi) {
|
|
7
7
|
return html`<h1>
|
|
8
8
|
${api.render("message1")}, ${api.render("message2")},
|
|
9
9
|
${api.render("message3")}!
|
package/templates/ts/src/main.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { mount } from "@inglorious/web"
|
|
2
2
|
|
|
3
|
+
import type { AppApi } from "../types"
|
|
3
4
|
import { App } from "./app"
|
|
4
5
|
import { store } from "./store"
|
|
5
6
|
|
|
6
|
-
mount(
|
|
7
|
+
mount(
|
|
8
|
+
store,
|
|
9
|
+
(api: AppApi) => App.render(null, api),
|
|
10
|
+
document.getElementById("root")!,
|
|
11
|
+
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createStore } from "@inglorious/store"
|
|
2
2
|
|
|
3
|
-
import type { AppEntity, AppState } from "../../types"
|
|
3
|
+
import type { AppEntity, AppEvents, AppState } from "../../types"
|
|
4
4
|
import { Message } from "../app/message"
|
|
5
5
|
import { entities } from "./entities"
|
|
6
6
|
import { middlewares } from "./middlewares"
|
|
7
7
|
|
|
8
|
-
export const store = createStore<AppEntity, AppState>({
|
|
8
|
+
export const store = createStore<AppEntity, AppState, AppEvents>({
|
|
9
9
|
types: { Message },
|
|
10
10
|
entities,
|
|
11
11
|
middlewares,
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import "./vite-env"
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BaseEntity,
|
|
5
|
+
EntitiesState,
|
|
6
|
+
EntityType,
|
|
7
|
+
EventsOf,
|
|
8
|
+
} from "@inglorious/store"
|
|
4
9
|
import type { Api, TemplateResult } from "@inglorious/web"
|
|
5
10
|
|
|
6
11
|
export interface MessageEntity extends BaseEntity {
|
|
@@ -17,7 +22,10 @@ export interface AppState extends EntitiesState<AppEntity> {
|
|
|
17
22
|
message3: MessageEntity
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
// The events the types below answer, so that `notify` checks them.
|
|
26
|
+
export type AppEvents = EventsOf<MessageType>
|
|
27
|
+
|
|
28
|
+
export type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
21
29
|
|
|
22
30
|
export type MessageType = {
|
|
23
31
|
click: (entity: MessageEntity) => void
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createStore } from "@inglorious/store"
|
|
2
2
|
|
|
3
|
-
import type { AppEntity, AppState } from "../../types"
|
|
3
|
+
import type { AppEntity, AppEvents, AppState } from "../../types"
|
|
4
4
|
import { Message } from "../app/message"
|
|
5
5
|
import { entities } from "./entities"
|
|
6
6
|
import { middlewares } from "./middlewares"
|
|
7
7
|
|
|
8
|
-
export const store = createStore<AppEntity, AppState>({
|
|
8
|
+
export const store = createStore<AppEntity, AppState, AppEvents>({
|
|
9
9
|
types: { Message },
|
|
10
10
|
entities,
|
|
11
11
|
middlewares,
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import "./vite-env"
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BaseEntity,
|
|
5
|
+
EntitiesState,
|
|
6
|
+
EntityType,
|
|
7
|
+
EventsOf,
|
|
8
|
+
} from "@inglorious/store"
|
|
4
9
|
import type { Api, TemplateResult } from "@inglorious/web"
|
|
5
10
|
|
|
6
11
|
export interface MessageEntity extends BaseEntity {
|
|
@@ -17,7 +22,10 @@ export interface AppState extends EntitiesState<AppEntity> {
|
|
|
17
22
|
message3: MessageEntity
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
// The events the types below answer, so that `notify` checks them.
|
|
26
|
+
export type AppEvents = EventsOf<MessageType>
|
|
27
|
+
|
|
28
|
+
export type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
21
29
|
|
|
22
30
|
export type MessageType = {
|
|
23
31
|
click: (entity: MessageEntity) => void
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { mount } from "@inglorious/web"
|
|
2
2
|
|
|
3
|
+
import type { AppApi } from "../types"
|
|
3
4
|
// @ts-expect-error - Handled by @inglorious/vite-plugin-vue
|
|
4
5
|
import { App } from "./app/index.vue"
|
|
5
6
|
import { store } from "./store"
|
|
6
7
|
|
|
7
|
-
mount(
|
|
8
|
+
mount(
|
|
9
|
+
store,
|
|
10
|
+
(api: AppApi) => App.render(null, api),
|
|
11
|
+
document.getElementById("root")!,
|
|
12
|
+
)
|