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.
Files changed (87) hide show
  1. package/README.md +61 -2
  2. package/dist/application/create.js +13 -2
  3. package/dist/application/generate.js +95 -0
  4. package/dist/args.js +1 -0
  5. package/dist/cli.js +41 -4
  6. package/dist/domain/create-plan.js +21 -3
  7. package/dist/domain/deploy-plan.js +82 -0
  8. package/dist/domain/form-dsl.js +2 -0
  9. package/dist/domain/remote-plan.js +156 -0
  10. package/package.json +4 -3
  11. package/templates/angular/README.md.hbs +46 -0
  12. package/templates/angular/_dependency-cruiser.mjs +51 -0
  13. package/templates/angular/apps/server/package.json +30 -0
  14. package/templates/angular/apps/server/src/app.ts +14 -0
  15. package/templates/angular/apps/server/src/index.ts +8 -0
  16. package/templates/angular/apps/server/test/app.test.ts +12 -0
  17. package/templates/angular/apps/server/tsconfig.json +7 -0
  18. package/templates/angular/apps/server/tsconfig.types.json +12 -0
  19. package/templates/angular/apps/shell/_editorconfig +17 -0
  20. package/templates/angular/apps/shell/angular.json +115 -0
  21. package/templates/angular/apps/shell/federation.config.mjs +44 -0
  22. package/templates/angular/apps/shell/package.json +42 -0
  23. package/templates/angular/apps/shell/public/federation.manifest.json +1 -0
  24. package/templates/angular/apps/shell/src/app/app.config.ts +20 -0
  25. package/templates/angular/apps/shell/src/app/app.css +4 -0
  26. package/templates/angular/apps/shell/src/app/app.html.hbs +16 -0
  27. package/templates/angular/apps/shell/src/app/app.routes.ts.hbs +39 -0
  28. package/templates/angular/apps/shell/src/app/app.ts +57 -0
  29. package/templates/angular/apps/shell/src/app/core/federation/load-remote.ts +31 -0
  30. package/templates/angular/apps/shell/src/app/core/federation/manifest.spec.ts +46 -0
  31. package/templates/angular/apps/shell/src/app/core/federation/manifest.ts +37 -0
  32. package/templates/angular/apps/shell/src/app/core/federation/remote-loader.ts +20 -0
  33. package/templates/angular/apps/shell/src/app/core/federation/timeout.ts +17 -0
  34. package/templates/angular/apps/shell/src/app/modules/home/home.component.ts.hbs +46 -0
  35. package/templates/angular/apps/shell/src/app/modules/unavailable/unavailable.component.ts +39 -0
  36. package/templates/angular/apps/shell/src/bootstrap.ts +25 -0
  37. package/templates/angular/apps/shell/src/environments/environment.development.ts +10 -0
  38. package/templates/angular/apps/shell/src/environments/environment.ts +10 -0
  39. package/templates/angular/apps/shell/src/index.html.hbs +12 -0
  40. package/templates/angular/apps/shell/src/main.ts +43 -0
  41. package/templates/angular/apps/shell/src/styles.css +19 -0
  42. package/templates/angular/apps/shell/tsconfig.app.json +10 -0
  43. package/templates/angular/apps/shell/tsconfig.federation.json +4 -0
  44. package/templates/angular/apps/shell/tsconfig.json.hbs +39 -0
  45. package/templates/angular/apps/shell/tsconfig.spec.json +9 -0
  46. package/templates/angular/bahtui.json +6 -0
  47. package/templates/angular/eslint.config.ts.hbs +28 -0
  48. package/templates/angular/knip.ts +27 -0
  49. package/templates/angular/package.json.hbs +52 -0
  50. package/templates/angular/packages/kit/package.json.hbs +27 -0
  51. package/templates/angular/packages/kit/src/api.ts +21 -0
  52. package/templates/angular/packages/kit/src/errors.ts +28 -0
  53. package/templates/angular/packages/kit/src/index.ts +2 -0
  54. package/templates/angular/packages/kit/test/errors.test.ts +28 -0
  55. package/templates/angular/packages/kit/tsconfig.json +9 -0
  56. package/templates/create/README.md.hbs +1 -1
  57. package/templates/create/_gitignore +1 -0
  58. package/templates/create/apps/web/index.html.hbs +8 -0
  59. package/templates/create/apps/web/package.json.hbs +1 -0
  60. package/templates/create/apps/web/src/App.tsx.hbs +18 -7
  61. package/templates/create/apps/web/src/app.css +27 -0
  62. package/templates/create/apps/web/src/health.test.ts.hbs +18 -4
  63. package/templates/create/apps/web/src/health.ts.hbs +8 -0
  64. package/templates/create/apps/web/src/main.tsx.hbs +12 -1
  65. package/templates/create/package.json.hbs +15 -5
  66. package/templates/deploy/deploy/__project__.service.hbs +23 -0
  67. package/templates/deploy/deploy/env.example.hbs +4 -0
  68. package/templates/deploy/deploy/install.sh.hbs +58 -0
  69. package/templates/deploy/deploy/nginx.conf.hbs +67 -0
  70. package/templates/deploy/scripts/deploy.ts.hbs +33 -0
  71. package/templates/deploy/scripts/preview.ts.hbs +57 -0
  72. package/templates/deploy/scripts/release.ts.hbs +48 -0
  73. package/templates/new/_gitignore +3 -0
  74. package/templates/new/_prettierignore +1 -0
  75. package/templates/new/package.json.hbs +2 -2
  76. package/templates/remote/_editorconfig +17 -0
  77. package/templates/remote/angular.json.hbs +95 -0
  78. package/templates/remote/federation.config.mjs.hbs +48 -0
  79. package/templates/remote/package.json.hbs +37 -0
  80. package/templates/remote/src/app/__name__.routes.ts.hbs +12 -0
  81. package/templates/remote/src/app/modules/__name__/__name__.component.ts.hbs +37 -0
  82. package/templates/remote/src/app/services/__name__.service.ts.hbs +14 -0
  83. package/templates/remote/src/index.html.hbs +11 -0
  84. package/templates/remote/src/main.ts.hbs +3 -0
  85. package/templates/remote/tsconfig.app.json +10 -0
  86. package/templates/remote/tsconfig.federation.json +4 -0
  87. package/templates/remote/tsconfig.json.hbs +36 -0
@@ -1,20 +1,31 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { api } from '@{{name}}/eden';
3
- import { describeHealth } from './health';
3
+ import { describeHealth, healthTone, type HealthTone } from './health';
4
+
5
+ type Status = { label: string; tone: HealthTone | 'neutral' };
4
6
 
5
7
  export default function App() {
6
- const [status, setStatus] = useState('checking api…');
8
+ const [status, setStatus] = useState<Status>({ label: 'checking api…', tone: 'neutral' });
7
9
 
8
10
  useEffect(() => {
9
- api.health.get().then(({ data, error }) => {
10
- setStatus(error ? `api unreachable (start it: bun run dev)` : describeHealth(data));
11
+ void api.health.get().then(({ data, error }) => {
12
+ setStatus(
13
+ error
14
+ ? { label: 'api unreachable (start it: bun run dev)', tone: 'danger' }
15
+ : { label: describeHealth(data), tone: healthTone(data) },
16
+ );
11
17
  });
12
18
  }, []);
13
19
 
14
20
  return (
15
- <main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
16
- <h1>{{name}}</h1>
17
- <p>{status}</p>
21
+ <main className="app">
22
+ <header className="app-bar">
23
+ <h1>{{name}}</h1>
24
+ <baht-theme-toggle />
25
+ </header>
26
+ <baht-card heading="API" subtitle="apps/api via libs/shared/eden">
27
+ <baht-badge tone={status.tone} label={status.label} />
28
+ </baht-card>
18
29
  </main>
19
30
  );
20
31
  }
@@ -0,0 +1,27 @@
1
+ /* Page chrome only. Components style themselves from the --baht-* tokens. */
2
+ body {
3
+ margin: 0;
4
+ background: var(--baht-color-bg);
5
+ color: var(--baht-color-text);
6
+ font-family: var(--baht-font-family);
7
+ }
8
+
9
+ .app {
10
+ max-width: 720px;
11
+ margin: 0 auto;
12
+ padding: var(--baht-space-8) var(--baht-space-4);
13
+ display: grid;
14
+ gap: var(--baht-space-4);
15
+ }
16
+
17
+ .app-bar {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ }
22
+
23
+ .app-bar h1 {
24
+ margin: 0;
25
+ font-family: var(--baht-font-display);
26
+ font-size: 1.5rem;
27
+ }
@@ -1,14 +1,28 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
- import { describeHealth } from './health';
2
+ import { describeHealth, healthTone } from './health';
3
+
4
+ const service = '{{name}}-api';
3
5
 
4
6
  describe('describeHealth', () => {
5
7
  it('formats a healthy response', () => {
6
- expect(describeHealth({ status: 'ok', service: '{{name}}-api' })).toBe(
7
- '{{name}}-api is ok',
8
- );
8
+ expect(describeHealth({ status: 'ok', service })).toBe(`${service} is ok`);
9
9
  });
10
10
 
11
11
  it('handles a missing response', () => {
12
12
  expect(describeHealth(null)).toBe('no response from api');
13
13
  });
14
14
  });
15
+
16
+ describe('healthTone', () => {
17
+ it('is success when the api says ok', () => {
18
+ expect(healthTone({ status: 'ok', service })).toBe('success');
19
+ });
20
+
21
+ it('is warning for any other status', () => {
22
+ expect(healthTone({ status: 'degraded', service })).toBe('warning');
23
+ });
24
+
25
+ it('is danger with no response', () => {
26
+ expect(healthTone(null)).toBe('danger');
27
+ });
28
+ });
@@ -1,6 +1,14 @@
1
1
  export type Health = { status: string; service: string } | null;
2
2
 
3
+ /** `<baht-badge>` status tones the health line uses. */
4
+ export type HealthTone = 'success' | 'danger' | 'warning';
5
+
3
6
  export function describeHealth(health: Health): string {
4
7
  if (!health) return 'no response from api';
5
8
  return `${health.service} is ${health.status}`;
6
9
  }
10
+
11
+ export function healthTone(health: Health): HealthTone {
12
+ if (!health) return 'danger';
13
+ return health.status === 'ok' ? 'success' : 'warning';
14
+ }
@@ -1,8 +1,19 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ // Register only the <baht-*> elements this app renders (one subpath per tag).
4
+ // `import 'bank20baht-ui'` registers all of them, at ~4x the bundle.
5
+ import 'bank20baht-ui/components/badge';
6
+ import 'bank20baht-ui/components/card';
7
+ import 'bank20baht-ui/components/theme-toggle';
8
+ import 'bank20baht-ui/react'; // JSX typings for every <baht-*>
9
+ import 'bank20baht-ui/tokens.css';
10
+ import './app.css';
3
11
  import App from './App';
4
12
 
5
- createRoot(document.getElementById('root')!).render(
13
+ const root = document.getElementById('root');
14
+ if (!root) throw new Error('#root missing from index.html');
15
+
16
+ createRoot(root).render(
6
17
  <StrictMode>
7
18
  <App />
8
19
  </StrictMode>,
@@ -2,7 +2,10 @@
2
2
  "name": "{{name}}-monorepo",
3
3
  "private": true,
4
4
  "type": "module",
5
- "workspaces": ["apps/*", "libs/**"],
5
+ "workspaces": [
6
+ "apps/*",
7
+ "libs/**"
8
+ ],
6
9
  "scripts": {
7
10
  "dev": "bun run --filter './apps/*' dev",
8
11
  "build": "bun run --filter './apps/*' build",
@@ -12,14 +15,21 @@
12
15
  "format": "prettier --write .",
13
16
  "format:check": "prettier --check .",
14
17
  "knip": "knip",
15
- "deps:circular": "madge --circular --extensions ts,tsx apps libs",
18
+ "deps:circular": "madge --circular --extensions ts,tsx --exclude '(^|/)(dist|node_modules)/' apps libs",
16
19
  "prepare": "husky"
17
20
  },
18
21
  "lint-staged": {
19
- "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
20
- "*.{css,json,js,mjs}": ["prettier --write"]
22
+ "*.{ts,tsx}": [
23
+ "eslint --fix",
24
+ "prettier --write"
25
+ ],
26
+ "*.{css,json,js,mjs}": [
27
+ "prettier --write"
28
+ ]
21
29
  },
22
- "trustedDependencies": ["esbuild"],
30
+ "trustedDependencies": [
31
+ "esbuild"
32
+ ],
23
33
  "devDependencies": {
24
34
  "@eslint/js": "^10.0.1",
25
35
  "eslint": "^10.6.0",
@@ -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,4 @@
1
+ # /etc/{{project}}/env, read by {{project}}.service. Created once by install.sh; edit it there.
2
+
3
+ # nginx is in front on the same machine and proxies /api/ here.
4
+ PORT={{apiPort}}
@@ -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/
@@ -0,0 +1,67 @@
1
+ # {{project}}: the web apps and their API behind one origin.
2
+ #
3
+ # / the host ({{host}})
4
+ # /remotes/<app>/ each remote, loaded by the host over Native Federation
5
+ # /federation.manifest.json which remotes exist; edit it to add or repoint one, no rebuild
6
+ # /api/ the Elysia server
7
+ #
8
+ # Installed as /etc/nginx/sites-available/{{project}} by deploy/install.sh.
9
+
10
+ upstream {{project}}_api {
11
+ server 127.0.0.1:{{apiPort}};
12
+ keepalive 16;
13
+ }
14
+
15
+ server {
16
+ listen 80 default_server;
17
+ listen [::]:80 default_server;
18
+ server_name _;
19
+
20
+ root /var/www/{{project}}/{{host}};
21
+ index index.html;
22
+
23
+ gzip on;
24
+ gzip_types text/css application/javascript application/json image/svg+xml;
25
+
26
+ client_max_body_size 1m;
27
+
28
+ location = /federation.manifest.json {
29
+ alias /etc/{{project}}/federation.manifest.json;
30
+ add_header Cache-Control no-store;
31
+ # Repeated in every location that sets a header: nginx drops the inherited ones there.
32
+ add_header X-Content-Type-Options nosniff always;
33
+ add_header X-Frame-Options SAMEORIGIN always;
34
+ add_header Referrer-Policy same-origin always;
35
+ }
36
+
37
+ # Entry points are re-checked on every load, so a new release shows up at once; the hashed
38
+ # chunks they point to never change and may be kept.
39
+ location /remotes/ {
40
+ root /var/www/{{project}};
41
+ try_files $uri =404;
42
+ add_header Cache-Control no-cache;
43
+ add_header X-Content-Type-Options nosniff always;
44
+ add_header X-Frame-Options SAMEORIGIN always;
45
+ add_header Referrer-Policy same-origin always;
46
+ }
47
+
48
+ location /api/ {
49
+ proxy_pass http://{{project}}_api;
50
+ proxy_http_version 1.1;
51
+ proxy_set_header Connection "";
52
+ proxy_set_header Host $host;
53
+ proxy_set_header X-Real-IP $remote_addr;
54
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
55
+ proxy_set_header X-Forwarded-Proto $scheme;
56
+ add_header X-Content-Type-Options nosniff always;
57
+ }
58
+
59
+ # The host is a single-page app: any other path is its router's business.
60
+ location / {
61
+ try_files $uri $uri/ /index.html;
62
+ add_header Cache-Control no-cache;
63
+ add_header X-Content-Type-Options nosniff always;
64
+ add_header X-Frame-Options SAMEORIGIN always;
65
+ add_header Referrer-Policy same-origin always;
66
+ }
67
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `bun run deploy root@host` builds a release, copies it to the machine and installs it
3
+ * (deploy/install.sh: nginx, bun and the {{project}} user on the first run; the files and a
4
+ * restart on every run). The target is a Debian or Ubuntu machine you can SSH into as root.
5
+ * Pass `--no-build` to ship the `.release/` that is already there.
6
+ */
7
+ import { RELEASE, buildRelease } from './release.ts'
8
+
9
+ const args = process.argv.slice(2)
10
+ const target = args.find((arg) => !arg.startsWith('--'))
11
+ if (target === undefined) {
12
+ console.error('usage: bun run deploy <user@host> [--no-build]')
13
+ process.exit(1)
14
+ }
15
+
16
+ const run = (command: string[]): void => {
17
+ console.log(`\x1b[36m$\x1b[0m ${command.join(' ')}`)
18
+ const result = Bun.spawnSync(command, { stdio: ['inherit', 'inherit', 'inherit'] })
19
+ if (result.exitCode !== 0) process.exit(result.exitCode)
20
+ }
21
+
22
+ if (!args.includes('--no-build')) buildRelease()
23
+
24
+ const REMOTE_DIR = '/opt/{{project}}-release'
25
+ // tar over ssh: the target needs nothing installed yet, not even rsync. Without macOS's extended
26
+ // attributes, which GNU tar on the other side only warns about.
27
+ run([
28
+ 'sh',
29
+ '-c',
30
+ `COPYFILE_DISABLE=1 tar --no-xattrs -czf - -C ${RELEASE} . | ssh ${target} 'rm -rf ${REMOTE_DIR} && mkdir -p ${REMOTE_DIR} && tar -xzf - -C ${REMOTE_DIR}'`,
31
+ ])
32
+ run(['ssh', target, `sh ${REMOTE_DIR}/install.sh`])
33
+ console.log(`deployed: http://${target.split('@').pop() ?? target}/`)
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `bun run preview` serves `.release/` the way deploy/nginx.conf does, on http://localhost:8080,
3
+ * with the API server started beside it: the production layout without a web server to install.
4
+ * Run `bun run release` first.
5
+ */
6
+ import { existsSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+
9
+ import { RELEASE } from './release.ts'
10
+
11
+ const PORT = 8080
12
+ const API_PORT = 3019
13
+ const API = `http://127.0.0.1:${String(API_PORT)}`
14
+ const WWW = join(RELEASE, 'www')
15
+
16
+ if (!existsSync(join(WWW, '{{host}}/index.html'))) {
17
+ console.error(`no ${RELEASE}/ yet: run \`bun run release\` first`)
18
+ process.exit(1)
19
+ }
20
+
21
+ const api = Bun.spawn(['bun', join(RELEASE, 'server/index.js')], {
22
+ env: { ...process.env, PORT: String(API_PORT) },
23
+ stdio: ['ignore', 'inherit', 'inherit'],
24
+ })
25
+ process.on('SIGINT', () => {
26
+ api.kill()
27
+ process.exit(0)
28
+ })
29
+
30
+ const file = async (path: string, cache: string): Promise<Response | undefined> => {
31
+ const found = Bun.file(path)
32
+ return (await found.exists())
33
+ ? new Response(found, { headers: { 'cache-control': cache } })
34
+ : undefined
35
+ }
36
+
37
+ const notFound = () => new Response('', { status: 404 })
38
+
39
+ Bun.serve({
40
+ port: PORT,
41
+ async fetch(request) {
42
+ const url = new URL(request.url)
43
+ const path = decodeURIComponent(url.pathname)
44
+ if (path.startsWith('/api/')) return fetch(new Request(`${API}${path}${url.search}`, request))
45
+ if (path.includes('..')) return new Response('bad path', { status: 400 })
46
+ if (path === '/federation.manifest.json') {
47
+ return (await file(join(RELEASE, 'federation.manifest.json'), 'no-store')) ?? notFound()
48
+ }
49
+ if (path.startsWith('/remotes/')) return (await file(join(WWW, path), 'no-cache')) ?? notFound()
50
+ return (
51
+ (path === '/' ? undefined : await file(join(WWW, '{{host}}', path), 'no-cache')) ??
52
+ (await file(join(WWW, '{{host}}/index.html'), 'no-cache')) ??
53
+ notFound()
54
+ )
55
+ },
56
+ })
57
+ console.log(`preview on http://localhost:${String(PORT)} (api on ${API})`)
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `bun run release` builds everything and lays it out the way the web server serves it, in
3
+ * `.release/`:
4
+ *
5
+ * server/index.js the API, one bundled file
6
+ * www/{{host}}/ the host
7
+ * www/remotes/<app>/ each remote in the host's federation manifest
8
+ * federation.manifest.json which remotes the host loads (installed once, then edited there)
9
+ * nginx.conf {{project}}.service env.example install.sh
10
+ *
11
+ * `scripts/deploy.ts` ships that folder; `scripts/preview.ts` serves it locally.
12
+ */
13
+ import { cpSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
14
+ import { join } from 'node:path'
15
+
16
+ export const RELEASE = '.release'
17
+ const HOST = 'apps/{{host}}'
18
+ const MANIFEST = join(HOST, 'public/federation.manifest.json')
19
+
20
+ const run = (command: string[]): void => {
21
+ console.log(`\x1b[36m$\x1b[0m ${command.join(' ')}`)
22
+ const result = Bun.spawnSync(command, { stdio: ['inherit', 'inherit', 'inherit'] })
23
+ if (result.exitCode !== 0) process.exit(result.exitCode)
24
+ }
25
+
26
+ export const buildRelease = (): void => {
27
+ run(['bun', 'run', 'build'])
28
+
29
+ rmSync(RELEASE, { recursive: true, force: true })
30
+ mkdirSync(join(RELEASE, 'www/remotes'), { recursive: true })
31
+ mkdirSync(join(RELEASE, 'server'), { recursive: true })
32
+
33
+ cpSync('apps/server/dist/index.js', join(RELEASE, 'server/index.js'))
34
+ cpSync(join(HOST, 'dist/{{host}}/browser'), join(RELEASE, 'www/{{host}}'), { recursive: true })
35
+ const remotes = Object.keys(JSON.parse(readFileSync(MANIFEST, 'utf8')) as Record<string, unknown>)
36
+ for (const app of remotes) {
37
+ cpSync(`apps/${app}/dist/${app}/browser`, join(RELEASE, 'www/remotes', app), {
38
+ recursive: true,
39
+ })
40
+ }
41
+ cpSync(MANIFEST, join(RELEASE, 'federation.manifest.json'))
42
+ for (const file of ['nginx.conf', '{{project}}.service', 'env.example', 'install.sh']) {
43
+ cpSync(join('deploy', file), join(RELEASE, file))
44
+ }
45
+ console.log(`release ready in ${RELEASE}/`)
46
+ }
47
+
48
+ if (import.meta.main) buildRelease()
@@ -1,6 +1,9 @@
1
1
  node_modules
2
2
  .zvec-grep
3
3
  dist
4
+ coverage
5
+ *.tsbuildinfo
6
+ .angular
4
7
  .env
5
8
  .env.*
6
9
  !.env.example
@@ -2,3 +2,4 @@ node_modules
2
2
  dist
3
3
  bun.lock
4
4
  coverage
5
+ .angular
@@ -15,7 +15,7 @@
15
15
  "format": "prettier --write .",
16
16
  "format:check": "prettier --check .",
17
17
  "knip": "knip",
18
- "madge": "madge --circular --extensions ts,tsx apps packages scripts",
18
+ "madge": "madge --circular --extensions ts,tsx --exclude '(^|/)(dist|node_modules|\\.angular)/' apps packages scripts",
19
19
  "deps": "depcruise apps packages scripts --config .dependency-cruiser.mjs",
20
20
  "dup": "jscpd",
21
21
  "audit": "bun audit --audit-level=high",
@@ -33,7 +33,7 @@
33
33
  "@eslint/css": "^2.0.0",
34
34
  "@eslint/js": "^10.0.1",
35
35
  "@types/bun": "^1.3.0",
36
- "bank20baht-strict": "0.0.3",
36
+ "bank20baht-strict": "0.0.6",
37
37
  "dependency-cruiser": "^18.3.1",
38
38
  "eslint": "^10.10.0",
39
39
  "eslint-config-prettier": "^10.1.8",
@@ -0,0 +1,17 @@
1
+ # Editor configuration, see https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.ts]
12
+ quote_type = single
13
+ ij_typescript_use_double_quotes = false
14
+
15
+ [*.md]
16
+ max_line_length = off
17
+ trim_trailing_whitespace = false
@@ -0,0 +1,95 @@
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "cli": {
5
+ "packageManager": "bun"
6
+ },
7
+ "newProjectRoot": "projects",
8
+ "projects": {
9
+ "{{app}}": {
10
+ "projectType": "application",
11
+ "schematics": {},
12
+ "root": "",
13
+ "sourceRoot": "src",
14
+ "prefix": "{{name}}",
15
+ "architect": {
16
+ "build": {
17
+ "builder": "@angular-architects/native-federation:build",
18
+ "options": {
19
+ "cacheExternalArtifacts": true,
20
+ "tsConfig": "tsconfig.federation.json"
21
+ },
22
+ "configurations": {
23
+ "production": {
24
+ "target": "{{app}}:esbuild:production"
25
+ },
26
+ "development": {
27
+ "target": "{{app}}:esbuild:development",
28
+ "dev": true
29
+ }
30
+ },
31
+ "defaultConfiguration": "production"
32
+ },
33
+ "serve": {
34
+ "builder": "@angular-architects/native-federation:build",
35
+ "options": {
36
+ "target": "{{app}}:serve-original:development",
37
+ "tsConfig": "tsconfig.federation.json",
38
+ "rebuildDelay": 500,
39
+ "cacheExternalArtifacts": true,
40
+ "dev": true,
41
+ "devServer": true,
42
+ "port": 0
43
+ }
44
+ },
45
+ "esbuild": {
46
+ "builder": "@angular/build:application",
47
+ "options": {
48
+ "browser": "src/main.ts",
49
+ "tsConfig": "tsconfig.app.json",
50
+ "styles": [],
51
+ "polyfills": ["es-module-shims"]
52
+ },
53
+ "configurations": {
54
+ "production": {
55
+ "budgets": [
56
+ {
57
+ "type": "initial",
58
+ "maximumWarning": "500kB",
59
+ "maximumError": "1MB"
60
+ },
61
+ {
62
+ "type": "anyComponentStyle",
63
+ "maximumWarning": "4kB",
64
+ "maximumError": "8kB"
65
+ }
66
+ ],
67
+ "outputHashing": "all"
68
+ },
69
+ "development": {
70
+ "optimization": false,
71
+ "extractLicenses": false,
72
+ "sourceMap": true
73
+ }
74
+ },
75
+ "defaultConfiguration": "production"
76
+ },
77
+ "serve-original": {
78
+ "builder": "@angular/build:dev-server",
79
+ "configurations": {
80
+ "production": {
81
+ "buildTarget": "{{app}}:esbuild:production"
82
+ },
83
+ "development": {
84
+ "buildTarget": "{{app}}:esbuild:development"
85
+ }
86
+ },
87
+ "defaultConfiguration": "development",
88
+ "options": {
89
+ "port": {{port}}
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }