bank20baht-cli 0.5.0 → 0.7.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 +61 -2
- package/dist/application/create.js +13 -2
- package/dist/application/generate.js +95 -0
- package/dist/args.js +1 -0
- package/dist/cli.js +41 -4
- package/dist/domain/create-plan.js +21 -3
- package/dist/domain/deploy-plan.js +82 -0
- package/dist/domain/form-dsl.js +2 -0
- package/dist/domain/remote-plan.js +156 -0
- package/package.json +4 -3
- package/templates/angular/README.md.hbs +46 -0
- package/templates/angular/_dependency-cruiser.mjs +51 -0
- package/templates/angular/apps/server/package.json +30 -0
- package/templates/angular/apps/server/src/app.ts +14 -0
- package/templates/angular/apps/server/src/index.ts +8 -0
- package/templates/angular/apps/server/test/app.test.ts +12 -0
- package/templates/angular/apps/server/tsconfig.json +7 -0
- package/templates/angular/apps/server/tsconfig.types.json +12 -0
- package/templates/angular/apps/shell/_editorconfig +17 -0
- package/templates/angular/apps/shell/angular.json +115 -0
- package/templates/angular/apps/shell/federation.config.mjs +44 -0
- package/templates/angular/apps/shell/package.json +42 -0
- package/templates/angular/apps/shell/public/federation.manifest.json +1 -0
- package/templates/angular/apps/shell/src/app/app.config.ts +20 -0
- package/templates/angular/apps/shell/src/app/app.css +4 -0
- package/templates/angular/apps/shell/src/app/app.html.hbs +16 -0
- package/templates/angular/apps/shell/src/app/app.routes.ts.hbs +39 -0
- package/templates/angular/apps/shell/src/app/app.ts +57 -0
- package/templates/angular/apps/shell/src/app/core/federation/load-remote.ts +31 -0
- package/templates/angular/apps/shell/src/app/core/federation/manifest.spec.ts +46 -0
- package/templates/angular/apps/shell/src/app/core/federation/manifest.ts +37 -0
- package/templates/angular/apps/shell/src/app/core/federation/remote-loader.ts +20 -0
- package/templates/angular/apps/shell/src/app/core/federation/timeout.ts +17 -0
- package/templates/angular/apps/shell/src/app/modules/home/home.component.ts.hbs +46 -0
- package/templates/angular/apps/shell/src/app/modules/unavailable/unavailable.component.ts +39 -0
- package/templates/angular/apps/shell/src/bootstrap.ts +25 -0
- package/templates/angular/apps/shell/src/environments/environment.development.ts +10 -0
- package/templates/angular/apps/shell/src/environments/environment.ts +10 -0
- package/templates/angular/apps/shell/src/index.html.hbs +12 -0
- package/templates/angular/apps/shell/src/main.ts +43 -0
- package/templates/angular/apps/shell/src/styles.css +19 -0
- package/templates/angular/apps/shell/tsconfig.app.json +10 -0
- package/templates/angular/apps/shell/tsconfig.federation.json +4 -0
- package/templates/angular/apps/shell/tsconfig.json.hbs +39 -0
- package/templates/angular/apps/shell/tsconfig.spec.json +9 -0
- package/templates/angular/bahtui.json +6 -0
- package/templates/angular/eslint.config.ts.hbs +28 -0
- package/templates/angular/knip.ts +27 -0
- package/templates/angular/package.json.hbs +52 -0
- package/templates/angular/packages/kit/package.json.hbs +27 -0
- package/templates/angular/packages/kit/src/api.ts +21 -0
- package/templates/angular/packages/kit/src/errors.ts +28 -0
- package/templates/angular/packages/kit/src/index.ts +2 -0
- package/templates/angular/packages/kit/test/errors.test.ts +28 -0
- package/templates/angular/packages/kit/tsconfig.json +9 -0
- package/templates/create/README.md.hbs +1 -1
- package/templates/create/_gitignore +1 -0
- package/templates/create/apps/web/index.html.hbs +8 -0
- package/templates/create/apps/web/package.json.hbs +1 -0
- package/templates/create/apps/web/src/App.tsx.hbs +18 -7
- package/templates/create/apps/web/src/app.css +27 -0
- package/templates/create/apps/web/src/health.test.ts.hbs +18 -4
- package/templates/create/apps/web/src/health.ts.hbs +8 -0
- package/templates/create/apps/web/src/main.tsx.hbs +12 -1
- package/templates/create/package.json.hbs +15 -5
- package/templates/deploy/deploy/__project__.service.hbs +23 -0
- package/templates/deploy/deploy/env.example.hbs +4 -0
- package/templates/deploy/deploy/install.sh.hbs +58 -0
- package/templates/deploy/deploy/nginx.conf.hbs +67 -0
- package/templates/deploy/scripts/deploy.ts.hbs +33 -0
- package/templates/deploy/scripts/preview.ts.hbs +57 -0
- package/templates/deploy/scripts/release.ts.hbs +48 -0
- package/templates/new/_gitignore +3 -0
- package/templates/new/_prettierignore +1 -0
- package/templates/new/package.json.hbs +2 -2
- package/templates/remote/_editorconfig +17 -0
- package/templates/remote/angular.json.hbs +95 -0
- package/templates/remote/federation.config.mjs.hbs +48 -0
- package/templates/remote/package.json.hbs +37 -0
- package/templates/remote/src/app/__name__.routes.ts.hbs +12 -0
- package/templates/remote/src/app/modules/__name__/__name__.component.ts.hbs +37 -0
- package/templates/remote/src/app/services/__name__.service.ts.hbs +14 -0
- package/templates/remote/src/index.html.hbs +11 -0
- package/templates/remote/src/main.ts.hbs +3 -0
- package/templates/remote/tsconfig.app.json +10 -0
- package/templates/remote/tsconfig.federation.json +4 -0
- package/templates/remote/tsconfig.json.hbs +36 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** One page a remote adds to the navigator. */
|
|
2
|
+
type RemoteNav = { label: string; path: string }
|
|
3
|
+
|
|
4
|
+
/** A microfrontend: where its code is, the URL prefix it owns and what it puts in the navigator. */
|
|
5
|
+
type RemoteEntry = {
|
|
6
|
+
/** URL of its `remoteEntry.json`. */
|
|
7
|
+
entry: string
|
|
8
|
+
/** Group heading in the navigator. */
|
|
9
|
+
title: string
|
|
10
|
+
/** First URL segment; everything under it is routed by the remote's `./Routes`. */
|
|
11
|
+
base: string
|
|
12
|
+
nav: RemoteNav[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Manifest = Record<string, RemoteEntry>
|
|
16
|
+
|
|
17
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
18
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
19
|
+
|
|
20
|
+
const isNav = (value: unknown): value is RemoteNav =>
|
|
21
|
+
isRecord(value) && typeof value['label'] === 'string' && typeof value['path'] === 'string'
|
|
22
|
+
|
|
23
|
+
const isEntry = (value: unknown): value is RemoteEntry =>
|
|
24
|
+
isRecord(value) &&
|
|
25
|
+
typeof value['entry'] === 'string' &&
|
|
26
|
+
typeof value['title'] === 'string' &&
|
|
27
|
+
typeof value['base'] === 'string' &&
|
|
28
|
+
/^[a-z0-9-]+$/.test(value['base']) &&
|
|
29
|
+
Array.isArray(value['nav']) &&
|
|
30
|
+
value['nav'].every(isNav)
|
|
31
|
+
|
|
32
|
+
/** The manifest when every entry has the right shape, else null: a broken file must not half-load. */
|
|
33
|
+
export const parseManifest = (json: unknown): Manifest | null => {
|
|
34
|
+
if (!isRecord(json)) return null
|
|
35
|
+
const entries = Object.entries(json)
|
|
36
|
+
return entries.every(([, value]) => isEntry(value)) ? (json as Manifest) : null
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core'
|
|
2
|
+
import type { NativeFederationResult } from '@angular-architects/native-federation'
|
|
3
|
+
import type { Manifest } from './manifest'
|
|
4
|
+
|
|
5
|
+
export type RemoteLoader = NativeFederationResult['loadRemoteModule']
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `loadRemoteModule` that `initFederation` resolved with in `main.ts`. Null when federation
|
|
9
|
+
* failed to start (or in unit tests); every remote route then shows the fallback page.
|
|
10
|
+
*/
|
|
11
|
+
export const REMOTE_LOADER = new InjectionToken<RemoteLoader | null>('REMOTE_LOADER', {
|
|
12
|
+
providedIn: 'root',
|
|
13
|
+
factory: () => null,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/** The remotes this host knows, as read at boot. */
|
|
17
|
+
export const MANIFEST = new InjectionToken<Manifest>('MANIFEST', {
|
|
18
|
+
providedIn: 'root',
|
|
19
|
+
factory: () => ({}),
|
|
20
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** `promise`, or a rejection once `ms` pass without it settling. */
|
|
2
|
+
export const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> =>
|
|
3
|
+
new Promise<T>((resolve, reject) => {
|
|
4
|
+
const timer = setTimeout(() => {
|
|
5
|
+
reject(new Error(`timed out after ${String(ms)} ms`))
|
|
6
|
+
}, ms)
|
|
7
|
+
promise.then(
|
|
8
|
+
(value) => {
|
|
9
|
+
clearTimeout(timer)
|
|
10
|
+
resolve(value)
|
|
11
|
+
},
|
|
12
|
+
(error: unknown) => {
|
|
13
|
+
clearTimeout(timer)
|
|
14
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
2
|
+
import { Router } from '@angular/router'
|
|
3
|
+
import { BAHT_UI } from 'bank20baht-ui-angular'
|
|
4
|
+
import { MANIFEST } from '../../core/federation/remote-loader'
|
|
5
|
+
|
|
6
|
+
/** The landing page: one card per remote in the federation manifest. */
|
|
7
|
+
@Component({
|
|
8
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9
|
+
imports: [BAHT_UI],
|
|
10
|
+
selector: 'shell-home',
|
|
11
|
+
styles: `
|
|
12
|
+
.remotes {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
15
|
+
gap: var(--baht-space-4);
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
template: `
|
|
19
|
+
<div class="app-page">
|
|
20
|
+
<baht-page-header
|
|
21
|
+
heading="{{name}}"
|
|
22
|
+
description="A Native Federation host. Each card is a remote, loaded at runtime."
|
|
23
|
+
></baht-page-header>
|
|
24
|
+
<div class="remotes">
|
|
25
|
+
@for (remote of remotes; track remote.base) {
|
|
26
|
+
<baht-card [heading]="remote.title">
|
|
27
|
+
<baht-button variant="secondary" (click)="go(remote.base)">Open</baht-button>
|
|
28
|
+
</baht-card>
|
|
29
|
+
} @empty {
|
|
30
|
+
<baht-empty-state
|
|
31
|
+
message="No remotes yet"
|
|
32
|
+
hint="Add one with bahtui g remote, then restart bun run dev."
|
|
33
|
+
></baht-empty-state>
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
`,
|
|
38
|
+
})
|
|
39
|
+
export class HomeComponent {
|
|
40
|
+
private readonly router = inject(Router)
|
|
41
|
+
protected readonly remotes = Object.values(inject(MANIFEST))
|
|
42
|
+
|
|
43
|
+
protected go(base: string): void {
|
|
44
|
+
void this.router.navigateByUrl(`/${base}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
|
2
|
+
import { BAHT_UI } from 'bank20baht-ui-angular'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shown when a remote's routes cannot be loaded over Native Federation, and for unknown URLs.
|
|
6
|
+
* The route's `data` sets the copy; all this page can offer is another try.
|
|
7
|
+
*/
|
|
8
|
+
@Component({
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
imports: [BAHT_UI],
|
|
11
|
+
selector: 'shell-unavailable',
|
|
12
|
+
styles: `
|
|
13
|
+
:host {
|
|
14
|
+
display: grid;
|
|
15
|
+
place-content: center;
|
|
16
|
+
min-height: 60dvh;
|
|
17
|
+
padding: var(--baht-space-4);
|
|
18
|
+
}
|
|
19
|
+
baht-error-state {
|
|
20
|
+
max-width: 440px;
|
|
21
|
+
}
|
|
22
|
+
`,
|
|
23
|
+
template: `
|
|
24
|
+
<baht-error-state
|
|
25
|
+
[attr.heading]="heading()"
|
|
26
|
+
[attr.message]="message()"
|
|
27
|
+
retry-label="Try again"
|
|
28
|
+
(bahtRetry)="retry()"
|
|
29
|
+
></baht-error-state>
|
|
30
|
+
`,
|
|
31
|
+
})
|
|
32
|
+
export class UnavailableComponent {
|
|
33
|
+
readonly heading = input('Something is unavailable')
|
|
34
|
+
readonly message = input('This page could not be loaded. Try again in a moment.')
|
|
35
|
+
|
|
36
|
+
protected retry(): void {
|
|
37
|
+
window.location.reload()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ApplicationRef } from '@angular/core'
|
|
2
|
+
import { bootstrapApplication } from '@angular/platform-browser'
|
|
3
|
+
import { appConfig } from './app/app.config'
|
|
4
|
+
import { App } from './app/app'
|
|
5
|
+
import type { Manifest } from './app/core/federation/manifest'
|
|
6
|
+
import { MANIFEST, REMOTE_LOADER, type RemoteLoader } from './app/core/federation/remote-loader'
|
|
7
|
+
|
|
8
|
+
export async function bootstrap(
|
|
9
|
+
loadRemoteModule: RemoteLoader | null,
|
|
10
|
+
manifest: Manifest,
|
|
11
|
+
): Promise<ApplicationRef> {
|
|
12
|
+
// Registers the <baht-*> custom elements in its own chunk; they upgrade in place once it
|
|
13
|
+
// lands. Shared with the remotes, so each tag is defined once.
|
|
14
|
+
void import('bank20baht-ui')
|
|
15
|
+
|
|
16
|
+
const config = appConfig(manifest)
|
|
17
|
+
return bootstrapApplication(App, {
|
|
18
|
+
...config,
|
|
19
|
+
providers: [
|
|
20
|
+
...config.providers,
|
|
21
|
+
{ provide: REMOTE_LOADER, useValue: loadRemoteModule },
|
|
22
|
+
{ provide: MANIFEST, useValue: manifest },
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Manifest } from '../app/core/federation/manifest'
|
|
2
|
+
|
|
3
|
+
/** The remotes on their dev servers (`bun run dev`). `bahtui g remote` adds an entry. */
|
|
4
|
+
const remotes: Manifest = {
|
|
5
|
+
// bahtui:remotes
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const federation: string | Manifest = remotes
|
|
9
|
+
|
|
10
|
+
export const federationFallback: Manifest = remotes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Manifest } from '../app/core/federation/manifest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the remotes load from: `public/federation.manifest.json`, read at runtime, so a remote can
|
|
5
|
+
* be added or repointed on the web server with no host build.
|
|
6
|
+
*/
|
|
7
|
+
export const federation: string | Manifest = '/federation.manifest.json'
|
|
8
|
+
|
|
9
|
+
/** What the host boots with when that file cannot be fetched: no remotes, the home page only. */
|
|
10
|
+
export const federationFallback: Manifest = {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { initFederation, type NativeFederationResult } from '@angular-architects/native-federation'
|
|
2
|
+
import { parseManifest, type Manifest } from './app/core/federation/manifest'
|
|
3
|
+
import { withTimeout } from './app/core/federation/timeout'
|
|
4
|
+
import { federation, federationFallback } from './environments/environment'
|
|
5
|
+
|
|
6
|
+
/** How long the shell waits for the manifest, then for the remotes, before booting without them. */
|
|
7
|
+
const BOOT_TIMEOUT_MS = 8_000
|
|
8
|
+
|
|
9
|
+
// Only federation starts here: shared packages (@angular/*, bank20baht-ui) resolve through the
|
|
10
|
+
// import map once initFederation is done, so everything else loads from ./bootstrap. A failed or
|
|
11
|
+
// slow start still boots the shell; the remotes' pages then show the fallback.
|
|
12
|
+
const manifest = async (): Promise<Manifest> => {
|
|
13
|
+
if (typeof federation !== 'string') return federation
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(federation, { signal: AbortSignal.timeout(BOOT_TIMEOUT_MS) })
|
|
16
|
+
if (!res.ok) throw new Error(`federation manifest: HTTP ${String(res.status)}`)
|
|
17
|
+
const parsed = parseManifest(await res.json())
|
|
18
|
+
if (parsed === null) throw new Error('federation manifest: an entry has the wrong shape')
|
|
19
|
+
return parsed
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('federation manifest failed to load, using the built-in one', err)
|
|
22
|
+
return federationFallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const start = async () => {
|
|
27
|
+
const remotes = await manifest()
|
|
28
|
+
const entries = Object.fromEntries(
|
|
29
|
+
Object.entries(remotes).map(([name, remote]) => [name, remote.entry]),
|
|
30
|
+
)
|
|
31
|
+
let nf: NativeFederationResult | null = null
|
|
32
|
+
try {
|
|
33
|
+
nf = await withTimeout(initFederation(entries), BOOT_TIMEOUT_MS)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(err)
|
|
36
|
+
}
|
|
37
|
+
const { bootstrap } = await import('./bootstrap')
|
|
38
|
+
await bootstrap(nf?.loadRemoteModule ?? null, remotes)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
start().catch((err: unknown) => {
|
|
42
|
+
console.error(err)
|
|
43
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* bank20baht-ui's tokens.css comes first (angular.json); this file only sets the page up. */
|
|
2
|
+
html,
|
|
3
|
+
body {
|
|
4
|
+
margin: 0;
|
|
5
|
+
min-height: 100dvh;
|
|
6
|
+
background: var(--baht-color-bg);
|
|
7
|
+
color: var(--baht-color-text);
|
|
8
|
+
font-family: var(--baht-font-family);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* The width every routed page keeps to inside the shell. */
|
|
12
|
+
.app-page {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: var(--baht-space-5);
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
max-width: 1200px;
|
|
18
|
+
padding: var(--baht-space-5);
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"types": []
|
|
7
|
+
},
|
|
8
|
+
"include": ["src/**/*.ts"],
|
|
9
|
+
"exclude": ["src/**/*.spec.ts"]
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"compileOnSave": false,
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"experimentalDecorators": true,
|
|
17
|
+
"importHelpers": true,
|
|
18
|
+
"target": "ES2022",
|
|
19
|
+
"module": "preserve",
|
|
20
|
+
"paths": {
|
|
21
|
+
"@{{name}}/kit": ["../../packages/kit/src/index.ts"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"angularCompilerOptions": {
|
|
25
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
26
|
+
"strictInjectionParameters": true,
|
|
27
|
+
"strictTemplates": true,
|
|
28
|
+
"strictInputAccessModifiers": true
|
|
29
|
+
},
|
|
30
|
+
"files": [],
|
|
31
|
+
"references": [
|
|
32
|
+
{
|
|
33
|
+
"path": "./tsconfig.app.json"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "./tsconfig.spec.json"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"types": ["vitest/globals"]
|
|
7
|
+
},
|
|
8
|
+
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineConfig } from 'eslint/config'
|
|
2
|
+
|
|
3
|
+
import { angular, bahtui, base } from 'bank20baht-strict/eslint'
|
|
4
|
+
|
|
5
|
+
/** The host, its remotes, and the kit they all compile in. `bahtui g remote` adds a line. */
|
|
6
|
+
const WEB = [
|
|
7
|
+
'apps/shell/src/**',
|
|
8
|
+
'packages/kit/src/**',
|
|
9
|
+
// bahtui:remotes
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
// Every rule is an error. Add app overrides below, each with a reason:
|
|
13
|
+
// // eslint-disable-next-line <rule> -- <why>
|
|
14
|
+
export default defineConfig([
|
|
15
|
+
...base,
|
|
16
|
+
...angular({
|
|
17
|
+
files: WEB,
|
|
18
|
+
restrictedImports: [
|
|
19
|
+
{
|
|
20
|
+
name: 'server',
|
|
21
|
+
allowTypeImports: true,
|
|
22
|
+
message:
|
|
23
|
+
'The web apps share the server only as types (the eden App type); no server runtime code in a bundle.',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
...bahtui({ files: WEB, tokenFiles: ['apps/shell/src/styles.css'] }),
|
|
28
|
+
])
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { knip } from 'bank20baht-strict/knip'
|
|
2
|
+
|
|
3
|
+
const SRC = ['src/**/*.ts']
|
|
4
|
+
|
|
5
|
+
/** A Native Federation remote: its entry is the routes module federation.config.mjs exposes. */
|
|
6
|
+
const remote = (name: string) => ({ entry: [`src/app/${name}.routes.ts`], project: SRC })
|
|
7
|
+
|
|
8
|
+
/** A workspace tested with `bun test`: the tests are its entries. */
|
|
9
|
+
const tested = { entry: ['test/**/*.test.ts'], project: [...SRC, 'test/**/*.ts'] }
|
|
10
|
+
|
|
11
|
+
export default knip({
|
|
12
|
+
ignoreDependencies: [
|
|
13
|
+
// The runtime loader behind @angular-architects/native-federation; nothing imports it directly.
|
|
14
|
+
'@softarc/native-federation-orchestrator',
|
|
15
|
+
// Reached through the kit; listed in each app so federation shares it as one singleton.
|
|
16
|
+
'ngx-eden',
|
|
17
|
+
// Angular's compiler emits tslib helpers (importHelpers).
|
|
18
|
+
'tslib',
|
|
19
|
+
],
|
|
20
|
+
workspaces: {
|
|
21
|
+
'.': { entry: ['scripts/*.ts'], project: ['scripts/**/*.ts'] },
|
|
22
|
+
'apps/server': tested,
|
|
23
|
+
'apps/shell': { project: SRC },
|
|
24
|
+
'packages/kit': tested,
|
|
25
|
+
// bahtui:remotes
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"apps/*",
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun run scripts/dev.ts",
|
|
11
|
+
"build": "bun run scripts/each.ts build",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.json && bun run scripts/each.ts typecheck",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"format:check": "prettier --check .",
|
|
17
|
+
"knip": "knip",
|
|
18
|
+
"madge": "madge --circular --extensions ts,tsx --exclude '(^|/)(dist|node_modules|\\.angular)/' apps packages scripts",
|
|
19
|
+
"deps": "depcruise apps packages scripts --config .dependency-cruiser.mjs",
|
|
20
|
+
"dup": "jscpd",
|
|
21
|
+
"audit": "bun audit --audit-level=high",
|
|
22
|
+
"secrets": "gitleaks git --redact --no-banner",
|
|
23
|
+
"test": "bun run scripts/each.ts test:coverage",
|
|
24
|
+
"check:fast": "bun run lint && bun run format:check && bun run typecheck && bun run madge && bun run deps",
|
|
25
|
+
"check": "bun run check:fast && bun run knip && bun run dup && bun run build && bun run test",
|
|
26
|
+
"prepare": "husky"
|
|
27
|
+
},
|
|
28
|
+
"prettier": "bank20baht-strict/prettier",
|
|
29
|
+
"packageManager": "bun@1.3.14",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@commitlint/cli": "^21.2.2",
|
|
32
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
33
|
+
"@eslint/css": "^2.0.0",
|
|
34
|
+
"@eslint/js": "^10.0.1",
|
|
35
|
+
"@types/bun": "^1.3.0",
|
|
36
|
+
"angular-eslint": "22.5.0",
|
|
37
|
+
"bank20baht-strict": "0.0.6",
|
|
38
|
+
"dependency-cruiser": "^18.3.1",
|
|
39
|
+
"eslint": "^10.10.0",
|
|
40
|
+
"eslint-config-prettier": "^10.1.8",
|
|
41
|
+
"eslint-plugin-sonarjs": "^4.2.1",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"jiti": "^2.6.1",
|
|
44
|
+
"jscpd": "^5.3.0",
|
|
45
|
+
"knip": "^6.35.1",
|
|
46
|
+
"lint-staged": "^17.5.1",
|
|
47
|
+
"madge": "^8.0.0",
|
|
48
|
+
"prettier": "^3.9.6",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"typescript-eslint": "^8.70.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}-kit",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "What the host and its remotes share: the typed API client and error text. Imported as @{{name}}/kit through a tsconfig path, so each app compiles its own copy.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"pretypecheck": "bun run --filter server build:types",
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"test:coverage": "bun test --coverage"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@angular/core": "^22.2.0",
|
|
18
|
+
"ngx-eden": "^0.2.0",
|
|
19
|
+
"rxjs": "~7.8.0",
|
|
20
|
+
"tslib": "^2.3.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "^1.3.0",
|
|
24
|
+
"server": "workspace:*",
|
|
25
|
+
"typescript": "~6.0.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Injectable, InjectionToken, inject, isDevMode } from '@angular/core'
|
|
2
|
+
import { createEden } from 'ngx-eden'
|
|
3
|
+
import type { App } from 'server'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where the API lives. In dev the Angular servers run on :4200 and up and Elysia on :3000;
|
|
7
|
+
* deployed, one web server serves the apps and proxies `/api` on the same origin.
|
|
8
|
+
*/
|
|
9
|
+
export const API_BASE = new InjectionToken<string>('API_BASE', {
|
|
10
|
+
providedIn: 'root',
|
|
11
|
+
factory: () => (isDevMode() ? 'http://localhost:3000' : globalThis.location.origin),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The typed client of the Elysia server: `inject(Api).client.api.hello.get()`.
|
|
16
|
+
* Types come from `apps/server/dist/types` (`bun run --filter server build:types`).
|
|
17
|
+
*/
|
|
18
|
+
@Injectable({ providedIn: 'root' })
|
|
19
|
+
export class Api {
|
|
20
|
+
readonly client = createEden<App>({ baseUrl: inject(API_BASE) })
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const FALLBACK = 'Something went wrong. Try again.'
|
|
2
|
+
|
|
3
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
typeof value === 'object' && value !== null
|
|
5
|
+
|
|
6
|
+
/** Where the response body sits: `value` from `unwrap()`, `error` on an `HttpErrorResponse`. */
|
|
7
|
+
const bodyOf = (error: Record<string, unknown>): Record<string, unknown> => {
|
|
8
|
+
for (const key of ['value', 'error']) {
|
|
9
|
+
const body = error[key]
|
|
10
|
+
if (isRecord(body)) return body
|
|
11
|
+
}
|
|
12
|
+
return error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The sentence to show for a failed API call: the server's `{ message }` or Elysia's validator
|
|
17
|
+
* `{ summary }`, as ngx-eden's `unwrap()` or a resource's `error()` carries it.
|
|
18
|
+
*/
|
|
19
|
+
export const describeError = (error: unknown): string => {
|
|
20
|
+
if (!isRecord(error)) return FALLBACK
|
|
21
|
+
if (error['status'] === 0) return 'The server cannot be reached.'
|
|
22
|
+
const body = bodyOf(error)
|
|
23
|
+
for (const key of ['message', 'summary']) {
|
|
24
|
+
const text = body[key]
|
|
25
|
+
if (typeof text === 'string' && text !== '') return text
|
|
26
|
+
}
|
|
27
|
+
return FALLBACK
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { describeError } from '../src/errors'
|
|
4
|
+
|
|
5
|
+
describe('describeError', () => {
|
|
6
|
+
test('reads the body ngx-eden wraps', () => {
|
|
7
|
+
expect(describeError({ status: 404, value: { message: 'Not found' } })).toBe('Not found')
|
|
8
|
+
expect(describeError({ status: 422, value: { summary: 'Expected string' } })).toBe(
|
|
9
|
+
'Expected string',
|
|
10
|
+
)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('reads the body of an HttpErrorResponse', () => {
|
|
14
|
+
expect(
|
|
15
|
+
describeError({ status: 404, error: { message: 'Gone' }, message: 'Http failure' }),
|
|
16
|
+
).toBe('Gone')
|
|
17
|
+
expect(describeError({ status: 0, message: 'Http failure response: 0 Unknown Error' })).toBe(
|
|
18
|
+
'The server cannot be reached.',
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('reads a plain Error and falls back', () => {
|
|
23
|
+
expect(describeError(new Error('boom'))).toBe('boom')
|
|
24
|
+
expect(describeError({ status: 0 })).toBe('The server cannot be reached.')
|
|
25
|
+
expect(describeError({ status: 500, value: 'oops' })).toBe('Something went wrong. Try again.')
|
|
26
|
+
expect(describeError(null)).toBe('Something went wrong. Try again.')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -13,7 +13,7 @@ bun run dev # web on :5173, api on :3001
|
|
|
13
13
|
## Layout (DDD taxonomy)
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
apps/web React 19 + Vite
|
|
16
|
+
apps/web React 19 + Vite on bank20baht-ui (import each <baht-*> you use in src/main.tsx)
|
|
17
17
|
apps/api Elysia — exports `type App`, the Eden type anchor
|
|
18
18
|
libs/shared/eden treaty<App> — the ONE typed http client
|
|
19
19
|
libs/domain/<d>/model (later) entities + form JSON, framework-free
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>{{name}}</title>
|
|
7
|
+
<script>
|
|
8
|
+
// apply the saved <baht-theme-toggle> choice before paint (no flash)
|
|
9
|
+
try {
|
|
10
|
+
var t = localStorage.getItem('baht-theme');
|
|
11
|
+
if (t === 'light' || t === 'dark')
|
|
12
|
+
document.documentElement.setAttribute('data-baht-theme', t);
|
|
13
|
+
} catch (e) {}
|
|
14
|
+
</script>
|
|
7
15
|
</head>
|
|
8
16
|
<body>
|
|
9
17
|
<div id="root"></div>
|