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
|
@@ -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(
|
|
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
|
|
16
|
-
<
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
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": [
|
|
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}": [
|
|
20
|
-
|
|
22
|
+
"*.{ts,tsx}": [
|
|
23
|
+
"eslint --fix",
|
|
24
|
+
"prettier --write"
|
|
25
|
+
],
|
|
26
|
+
"*.{css,json,js,mjs}": [
|
|
27
|
+
"prettier --write"
|
|
28
|
+
]
|
|
21
29
|
},
|
|
22
|
-
"trustedDependencies": [
|
|
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,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()
|
package/templates/new/_gitignore
CHANGED
|
@@ -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.
|
|
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
|
+
}
|