bank20baht-cli 0.6.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 +59 -0
- 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/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/_gitignore +1 -0
- package/templates/create/apps/web/package.json.hbs +1 -1
- package/templates/create/package.json.hbs +1 -1
- 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,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
|
+
})
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"format": "prettier --write .",
|
|
16
16
|
"format:check": "prettier --check .",
|
|
17
17
|
"knip": "knip",
|
|
18
|
-
"deps:circular": "madge --circular --extensions ts,tsx apps libs",
|
|
18
|
+
"deps:circular": "madge --circular --extensions ts,tsx --exclude '(^|/)(dist|node_modules)/' apps libs",
|
|
19
19
|
"prepare": "husky"
|
|
20
20
|
},
|
|
21
21
|
"lint-staged": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description={{project}} API server
|
|
3
|
+
After=network-online.target
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
User={{project}}
|
|
8
|
+
Group={{project}}
|
|
9
|
+
WorkingDirectory=/var/lib/{{project}}
|
|
10
|
+
EnvironmentFile=/etc/{{project}}/env
|
|
11
|
+
ExecStart=/usr/local/bin/bun /opt/{{project}}/server/index.js
|
|
12
|
+
Restart=on-failure
|
|
13
|
+
RestartSec=3
|
|
14
|
+
|
|
15
|
+
# It only needs its own folder and the network.
|
|
16
|
+
NoNewPrivileges=true
|
|
17
|
+
ProtectSystem=strict
|
|
18
|
+
ProtectHome=true
|
|
19
|
+
PrivateTmp=true
|
|
20
|
+
ReadWritePaths=/var/lib/{{project}}
|
|
21
|
+
|
|
22
|
+
[Install]
|
|
23
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Runs on the target, as root, from the unpacked release (scripts/deploy.ts does that).
|
|
3
|
+
# Safe to run again: the first run sets the machine up, every run installs the release.
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
RELEASE=$(cd "$(dirname "$0")" && pwd)
|
|
7
|
+
APP={{project}}
|
|
8
|
+
|
|
9
|
+
if ! command -v nginx >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
|
|
10
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
11
|
+
apt-get update -qq
|
|
12
|
+
apt-get install -y -qq nginx curl unzip ca-certificates
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [ ! -x /usr/local/bin/bun ]; then
|
|
16
|
+
# bun's installer is a bash script (dash, Debian's sh, refuses its `set -o pipefail`).
|
|
17
|
+
curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash >/dev/null
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
id "$APP" >/dev/null 2>&1 || useradd --system --home "/var/lib/$APP" --shell /usr/sbin/nologin "$APP"
|
|
21
|
+
install -d -o "$APP" -g "$APP" "/var/lib/$APP"
|
|
22
|
+
install -d "/etc/$APP" "/opt/$APP" "/var/www/$APP"
|
|
23
|
+
|
|
24
|
+
# Settings are written once and never overwritten: they hold what was set on this machine.
|
|
25
|
+
if [ ! -f "/etc/$APP/env" ]; then
|
|
26
|
+
install -m 640 -o root -g "$APP" "$RELEASE/env.example" "/etc/$APP/env"
|
|
27
|
+
echo "created /etc/$APP/env"
|
|
28
|
+
fi
|
|
29
|
+
if [ ! -f "/etc/$APP/federation.manifest.json" ]; then
|
|
30
|
+
install -m 644 "$RELEASE/federation.manifest.json" "/etc/$APP/federation.manifest.json"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# The release itself: swap whole folders so a half-copied one is never served.
|
|
34
|
+
for part in server www; do
|
|
35
|
+
rm -rf "/opt/$APP/$part.new"
|
|
36
|
+
cp -R "$RELEASE/$part" "/opt/$APP/$part.new"
|
|
37
|
+
done
|
|
38
|
+
rm -rf "/opt/$APP/server.old" && { [ ! -d "/opt/$APP/server" ] || mv "/opt/$APP/server" "/opt/$APP/server.old"; }
|
|
39
|
+
mv "/opt/$APP/server.new" "/opt/$APP/server"
|
|
40
|
+
rm -rf "/var/www/$APP.old" && mv "/var/www/$APP" "/var/www/$APP.old"
|
|
41
|
+
mv "/opt/$APP/www.new" "/var/www/$APP"
|
|
42
|
+
rm -rf "/var/www/$APP.old" "/opt/$APP/server.old"
|
|
43
|
+
|
|
44
|
+
install -m 644 "$RELEASE/$APP.service" "/etc/systemd/system/$APP.service"
|
|
45
|
+
install -m 644 "$RELEASE/nginx.conf" "/etc/nginx/sites-available/$APP"
|
|
46
|
+
ln -sf "/etc/nginx/sites-available/$APP" "/etc/nginx/sites-enabled/$APP"
|
|
47
|
+
rm -f /etc/nginx/sites-enabled/default
|
|
48
|
+
|
|
49
|
+
systemctl daemon-reload
|
|
50
|
+
systemctl enable "$APP" >/dev/null 2>&1
|
|
51
|
+
systemctl restart "$APP"
|
|
52
|
+
nginx -t
|
|
53
|
+
systemctl reload nginx || systemctl restart nginx
|
|
54
|
+
|
|
55
|
+
sleep 2
|
|
56
|
+
systemctl is-active "$APP"
|
|
57
|
+
curl -sS -o /dev/null -w 'api %{http_code}\n' http://127.0.0.1:{{apiPort}}/api/
|
|
58
|
+
curl -fsS -o /dev/null -w 'host %{http_code}\n' http://127.0.0.1/
|