@jskit-ai/create-app 0.1.93 → 0.1.95
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/package.json +2 -2
- package/src/server/index.js +8 -1
- package/templates/base-shell/.jskit/lock.json +229 -0
- package/templates/base-shell/package.json +4 -2
- package/templates/base-shell/packages/main/src/client/providers/MainClientProvider.js +8 -0
- package/templates/base-shell/src/App.vue +7 -9
- package/templates/base-shell/src/components/ShellLayout.vue +12 -0
- package/templates/base-shell/src/components/menus/MenuLinkItem.vue +30 -0
- package/templates/base-shell/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/templates/base-shell/src/components/menus/TabLinkItem.vue +42 -0
- package/templates/base-shell/src/error.js +19 -0
- package/templates/base-shell/src/pages/home/index.vue +85 -21
- package/templates/base-shell/src/pages/home/settings/general/index.vue +40 -0
- package/templates/base-shell/src/pages/home/settings/index.vue +7 -0
- package/templates/base-shell/src/pages/home/settings.vue +109 -0
- package/templates/base-shell/src/pages/home.vue +8 -1
- package/templates/base-shell/src/placement.js +56 -0
- package/templates/base-shell/src/placementTopology.js +149 -0
- package/templates/base-shell/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/templates/base-shell/tests/e2e/base-shell.spec.ts +1 -1
- package/templates/base-shell/vite.config.mjs +14 -0
- package/templates/minimal-shell/.github/workflows/verify.yml +29 -0
- package/templates/minimal-shell/.jskit/lock.json +31 -0
- package/templates/minimal-shell/AGENTS.md +10 -0
- package/templates/minimal-shell/Procfile +2 -0
- package/templates/minimal-shell/app.json +14 -0
- package/templates/minimal-shell/bin/server.js +8 -0
- package/templates/minimal-shell/config/public.js +41 -0
- package/templates/minimal-shell/config/server.js +1 -0
- package/templates/minimal-shell/config/surfaceAccessPolicies.js +3 -0
- package/templates/minimal-shell/eslint.config.mjs +19 -0
- package/templates/minimal-shell/favicon.svg +7 -0
- package/templates/minimal-shell/gitignore +8 -0
- package/templates/minimal-shell/index.html +13 -0
- package/templates/minimal-shell/jsconfig.json +8 -0
- package/templates/minimal-shell/package.json +55 -0
- package/templates/minimal-shell/packages/main/package.descriptor.mjs +69 -0
- package/templates/minimal-shell/packages/main/package.json +12 -0
- package/templates/minimal-shell/packages/main/src/client/index.js +13 -0
- package/templates/minimal-shell/packages/main/src/client/providers/MainClientProvider.js +20 -0
- package/templates/minimal-shell/packages/main/src/server/MainServiceProvider.js +20 -0
- package/templates/minimal-shell/packages/main/src/server/index.js +1 -0
- package/templates/minimal-shell/packages/main/src/server/loadAppConfig.js +9 -0
- package/templates/minimal-shell/packages/main/src/shared/index.js +10 -0
- package/templates/minimal-shell/packages/main/src/shared/schemas/index.js +22 -0
- package/templates/minimal-shell/scripts/dokku-set-remote-envs-via-ssh.sh +210 -0
- package/templates/minimal-shell/server/lib/runtimeEnv.js +45 -0
- package/templates/minimal-shell/server/lib/surfaceRuntime.js +10 -0
- package/templates/minimal-shell/server.js +189 -0
- package/templates/minimal-shell/src/App.vue +13 -0
- package/templates/minimal-shell/src/main.js +85 -0
- package/templates/minimal-shell/src/pages/home/index.vue +48 -0
- package/templates/minimal-shell/src/pages/home.vue +13 -0
- package/templates/minimal-shell/src/views/NotFound.vue +13 -0
- package/templates/minimal-shell/tests/client/smoke.vitest.js +7 -0
- package/templates/minimal-shell/tests/e2e/base-shell.spec.ts +57 -0
- package/templates/minimal-shell/tests/server/minimalShell.validator.test.js +131 -0
- package/templates/minimal-shell/tests/server/smoke.test.js +16 -0
- package/templates/minimal-shell/vite.config.mjs +84 -0
- package/templates/minimal-shell/vite.shared.mjs +59 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@local/main",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
kind: "runtime",
|
|
6
|
+
description: "App-local main composition and glue scaffold.",
|
|
7
|
+
dependsOn: [],
|
|
8
|
+
capabilities: {
|
|
9
|
+
provides: [],
|
|
10
|
+
requires: []
|
|
11
|
+
},
|
|
12
|
+
options: {},
|
|
13
|
+
runtime: {
|
|
14
|
+
server: {
|
|
15
|
+
providerEntrypoint: "src/server/MainServiceProvider.js",
|
|
16
|
+
providers: [
|
|
17
|
+
{
|
|
18
|
+
entrypoint: "src/server/MainServiceProvider.js",
|
|
19
|
+
export: "MainServiceProvider"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
client: {
|
|
24
|
+
providers: [
|
|
25
|
+
{
|
|
26
|
+
entrypoint: "src/client/providers/MainClientProvider.js",
|
|
27
|
+
export: "MainClientProvider"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
jskit: {
|
|
34
|
+
ownershipGuidance: {
|
|
35
|
+
title: "App-local main lane",
|
|
36
|
+
summary: "Keep @local/main focused on app composition and lightweight glue. Substantial server features should become dedicated packages instead of growing inside packages/main.",
|
|
37
|
+
responsibilities: [
|
|
38
|
+
"packages/main server code: bootstraps app-local configuration and lightweight wiring only",
|
|
39
|
+
"substantial non-CRUD server features: scaffold a dedicated package with feature-server-generator",
|
|
40
|
+
"packages/main: do not add service/controller/route/repository feature trees here"
|
|
41
|
+
],
|
|
42
|
+
examples: [
|
|
43
|
+
"jskit generate feature-server-generator scaffold booking-engine",
|
|
44
|
+
"jskit generate feature-server-generator scaffold availability-engine --mode orchestrator"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
server: {
|
|
49
|
+
routes: []
|
|
50
|
+
},
|
|
51
|
+
ui: {
|
|
52
|
+
routes: [],
|
|
53
|
+
elements: [],
|
|
54
|
+
overrides: []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
mutations: {
|
|
58
|
+
dependencies: {
|
|
59
|
+
runtime: {},
|
|
60
|
+
dev: {}
|
|
61
|
+
},
|
|
62
|
+
packageJson: {
|
|
63
|
+
scripts: {}
|
|
64
|
+
},
|
|
65
|
+
procfile: {},
|
|
66
|
+
text: [],
|
|
67
|
+
files: []
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@local/main",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./client": "./src/client/index.js",
|
|
8
|
+
"./server": "./src/server/index.js",
|
|
9
|
+
"./server/MainServiceProvider": "./src/server/MainServiceProvider.js",
|
|
10
|
+
"./shared": "./src/shared/index.js"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client entrypoint for @local/main.
|
|
3
|
+
*
|
|
4
|
+
* Export browser-facing modules from here.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
* export { MainClientProvider } from "./providers/MainClientProvider.js";
|
|
8
|
+
* export { registerMainClientComponent } from "./providers/MainClientProvider.js";
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
MainClientProvider,
|
|
12
|
+
registerMainClientComponent
|
|
13
|
+
} from "./providers/MainClientProvider.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const mainClientComponents = [];
|
|
2
|
+
|
|
3
|
+
function registerMainClientComponent(token, resolveComponent) {
|
|
4
|
+
mainClientComponents.push({ token, resolveComponent });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class MainClientProvider {
|
|
8
|
+
static id = "local.main.client";
|
|
9
|
+
|
|
10
|
+
register(app) {
|
|
11
|
+
for (const { token, resolveComponent } of mainClientComponents) {
|
|
12
|
+
app.singleton(token, resolveComponent);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
MainClientProvider,
|
|
19
|
+
registerMainClientComponent
|
|
20
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { loadAppConfig } from "./loadAppConfig.js";
|
|
2
|
+
|
|
3
|
+
class MainServiceProvider {
|
|
4
|
+
static id = "local.main";
|
|
5
|
+
|
|
6
|
+
// Register only lightweight app-local composition bindings here.
|
|
7
|
+
async register(app) {
|
|
8
|
+
const appConfig = await loadAppConfig({
|
|
9
|
+
moduleUrl: import.meta.url
|
|
10
|
+
});
|
|
11
|
+
app.instance("appConfig", appConfig);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Keep packages/main as app glue only.
|
|
15
|
+
// When backend behavior becomes substantial, scaffold a dedicated package:
|
|
16
|
+
// jskit generate feature-server-generator scaffold <feature-name>
|
|
17
|
+
boot() {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { MainServiceProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MainServiceProvider } from "./MainServiceProvider.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-local shared transport validators/resources live here.
|
|
3
|
+
* Dedicated feature packages should keep their own schemas with the feature.
|
|
4
|
+
*
|
|
5
|
+
* Example:
|
|
6
|
+
* import { createSchema } from "json-rest-schema";
|
|
7
|
+
*
|
|
8
|
+
* export const helloQuerySchema = createSchema({
|
|
9
|
+
* name: { type: "string", minLength: 1, maxLength: 80 }
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* export const helloResponseSchema = {
|
|
13
|
+
* type: "object",
|
|
14
|
+
* additionalProperties: false,
|
|
15
|
+
* required: ["ok", "message"],
|
|
16
|
+
* properties: {
|
|
17
|
+
* ok: { type: "boolean" },
|
|
18
|
+
* message: { type: "string", minLength: 1 }
|
|
19
|
+
* }
|
|
20
|
+
* };
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
Usage:
|
|
7
|
+
scripts/dokku-set-remote-envs-via-ssh.sh <ssh-target> <public-host>
|
|
8
|
+
|
|
9
|
+
Reads the allowlisted deploy env vars from .env, derives APP_PUBLIC_URL from
|
|
10
|
+
the public host, and pushes them to the remote Dokku app with --no-restart.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
scripts/dokku-set-remote-envs-via-ssh.sh dokku@example.com beepollen.appgenius.biz
|
|
14
|
+
scripts/dokku-set-remote-envs-via-ssh.sh dokku@example.com https://beepollen.appgenius.biz
|
|
15
|
+
EOF
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
die() {
|
|
19
|
+
printf 'Error: %s\n' "$*" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
require_cmd() {
|
|
24
|
+
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resolve_script_dir() {
|
|
28
|
+
cd -- "$(dirname -- "$0")" && pwd
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if [[ $# -eq 0 ]]; then
|
|
32
|
+
usage
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
case "${1:-}" in
|
|
37
|
+
-h|--help)
|
|
38
|
+
usage
|
|
39
|
+
exit 0
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
if [[ $# -ne 2 ]]; then
|
|
44
|
+
usage
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
require_cmd ssh
|
|
49
|
+
require_cmd node
|
|
50
|
+
|
|
51
|
+
SSH_TARGET="$1"
|
|
52
|
+
PUBLIC_HOST_INPUT="$2"
|
|
53
|
+
SCRIPT_DIR="$(resolve_script_dir)"
|
|
54
|
+
APP_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
55
|
+
|
|
56
|
+
case "$PUBLIC_HOST_INPUT" in
|
|
57
|
+
http://*|https://*)
|
|
58
|
+
APP_PUBLIC_URL="$PUBLIC_HOST_INPUT"
|
|
59
|
+
;;
|
|
60
|
+
*)
|
|
61
|
+
APP_PUBLIC_URL="https://${PUBLIC_HOST_INPUT}"
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
APP_NAME="$(node - "$APP_ROOT" <<'NODE'
|
|
66
|
+
const fs = require("node:fs");
|
|
67
|
+
const path = require("node:path");
|
|
68
|
+
|
|
69
|
+
const appRoot = process.argv[2];
|
|
70
|
+
|
|
71
|
+
function readNameFromJson(relativePath) {
|
|
72
|
+
const absolutePath = path.join(appRoot, relativePath);
|
|
73
|
+
if (!fs.existsSync(absolutePath)) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
const source = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
77
|
+
return String(source?.name || "").trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const appName = readNameFromJson("app.json") || readNameFromJson("package.json");
|
|
81
|
+
if (!appName) {
|
|
82
|
+
console.error("Unable to resolve app name from app.json or package.json.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.stdout.write(appName);
|
|
87
|
+
NODE
|
|
88
|
+
)"
|
|
89
|
+
|
|
90
|
+
set_remote_env() {
|
|
91
|
+
local key="$1"
|
|
92
|
+
local encoded_value="$2"
|
|
93
|
+
|
|
94
|
+
ssh "$SSH_TARGET" /bin/bash -s -- "$APP_NAME" "$key" "$encoded_value" <<'EOF'
|
|
95
|
+
set -Eeuo pipefail
|
|
96
|
+
|
|
97
|
+
APP_NAME="$1"
|
|
98
|
+
KEY="$2"
|
|
99
|
+
ENCODED_VALUE="$3"
|
|
100
|
+
|
|
101
|
+
command -v dokku >/dev/null 2>&1 || {
|
|
102
|
+
printf 'dokku command not found on remote host.\n' >&2
|
|
103
|
+
exit 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dokku config:set --no-restart --encoded "$APP_NAME" "$KEY=$ENCODED_VALUE" >/dev/null
|
|
107
|
+
EOF
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while IFS=$'\t' read -r key encoded_value; do
|
|
111
|
+
[[ -n "$key" ]] || continue
|
|
112
|
+
set_remote_env "$key" "$encoded_value"
|
|
113
|
+
printf 'Set %s on %s\n' "$key" "$APP_NAME"
|
|
114
|
+
done < <(node - "$APP_ROOT" "$APP_PUBLIC_URL" <<'NODE'
|
|
115
|
+
const fs = require("node:fs");
|
|
116
|
+
const path = require("node:path");
|
|
117
|
+
|
|
118
|
+
const appRoot = process.argv[2];
|
|
119
|
+
const appPublicUrl = String(process.argv[3] || "").trim();
|
|
120
|
+
const envPath = path.join(appRoot, ".env");
|
|
121
|
+
|
|
122
|
+
const requiredKeys = Object.freeze([
|
|
123
|
+
"AUTH_PROFILE_MODE",
|
|
124
|
+
"AUTH_SUPABASE_URL",
|
|
125
|
+
"AUTH_SUPABASE_PUBLISHABLE_KEY"
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const optionalKeys = Object.freeze([
|
|
129
|
+
"AI_PROVIDER",
|
|
130
|
+
"AI_API_KEY",
|
|
131
|
+
"AI_BASE_URL",
|
|
132
|
+
"AI_TIMEOUT_MS",
|
|
133
|
+
"AUTH_OAUTH_PROVIDERS",
|
|
134
|
+
"AUTH_OAUTH_DEFAULT_PROVIDER",
|
|
135
|
+
"AUTH_JWT_AUDIENCE"
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
function parseDotEnv(source) {
|
|
139
|
+
const result = new Map();
|
|
140
|
+
|
|
141
|
+
for (const rawLine of String(source || "").split(/\r?\n/u)) {
|
|
142
|
+
const line = rawLine.trim();
|
|
143
|
+
if (!line || line.startsWith("#")) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const normalizedLine = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
148
|
+
const equalsIndex = normalizedLine.indexOf("=");
|
|
149
|
+
if (equalsIndex < 1) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const key = normalizedLine.slice(0, equalsIndex).trim();
|
|
154
|
+
let value = normalizedLine.slice(equalsIndex + 1).trim();
|
|
155
|
+
if (
|
|
156
|
+
value.length >= 2 &&
|
|
157
|
+
((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))
|
|
158
|
+
) {
|
|
159
|
+
value = value.slice(1, -1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
result.set(key, value);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toBase64(value) {
|
|
169
|
+
return Buffer.from(String(value || ""), "utf8").toString("base64");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!fs.existsSync(envPath)) {
|
|
173
|
+
console.error("Missing .env in app root.");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!appPublicUrl) {
|
|
178
|
+
console.error("APP_PUBLIC_URL is required.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const envMap = parseDotEnv(fs.readFileSync(envPath, "utf8"));
|
|
183
|
+
const rows = [["APP_PUBLIC_URL", toBase64(appPublicUrl)]];
|
|
184
|
+
|
|
185
|
+
for (const key of requiredKeys) {
|
|
186
|
+
const value = String(envMap.get(key) || "").trim();
|
|
187
|
+
if (!value) {
|
|
188
|
+
console.error(`Missing required .env value: ${key}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
rows.push([key, toBase64(value)]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const key of optionalKeys) {
|
|
195
|
+
const value = String(envMap.get(key) || "").trim();
|
|
196
|
+
if (!value) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
rows.push([key, toBase64(value)]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const [key, encodedValue] of rows) {
|
|
203
|
+
process.stdout.write(`${key}\t${encodedValue}\n`);
|
|
204
|
+
}
|
|
205
|
+
NODE
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
printf '\nDone.\n'
|
|
209
|
+
printf 'Restart when ready:\n'
|
|
210
|
+
printf ' ssh %q %q\n' "$SSH_TARGET" "dokku ps:restart $APP_NAME"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { surfaceRuntime } from "./surfaceRuntime.js";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
function toPort(value, fallback = 3000) {
|
|
7
|
+
const parsed = Number.parseInt(String(value || "").trim(), 10);
|
|
8
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let envLoaded = false;
|
|
15
|
+
|
|
16
|
+
function ensureRuntimeEnvLoaded() {
|
|
17
|
+
if (envLoaded) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const dotenvModule = require("dotenv");
|
|
22
|
+
const loadDotEnv = dotenvModule?.config;
|
|
23
|
+
if (typeof loadDotEnv === "function") {
|
|
24
|
+
loadDotEnv();
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// dotenv is optional in base-shell; bundles can add it when needed.
|
|
28
|
+
}
|
|
29
|
+
envLoaded = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveRuntimeEnv() {
|
|
33
|
+
ensureRuntimeEnvLoaded();
|
|
34
|
+
const serverSurface = surfaceRuntime.normalizeSurfaceMode(
|
|
35
|
+
process.env.JSKIT_SERVER_SURFACE || process.env.SERVER_SURFACE
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
...process.env,
|
|
39
|
+
SERVER_SURFACE: serverSurface,
|
|
40
|
+
PORT: toPort(process.env.PORT, 3000),
|
|
41
|
+
HOST: String(process.env.HOST || "").trim() || "0.0.0.0"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { resolveRuntimeEnv };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createSurfaceRuntime } from "@jskit-ai/kernel/shared/surface/runtime";
|
|
2
|
+
import { config } from "../../config/public.js";
|
|
3
|
+
|
|
4
|
+
const surfaceRuntime = createSurfaceRuntime({
|
|
5
|
+
allMode: config.surfaceModeAll,
|
|
6
|
+
surfaces: config.surfaceDefinitions,
|
|
7
|
+
defaultSurfaceId: config.surfaceDefaultId
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export { surfaceRuntime };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import fastifyStatic from "@fastify/static";
|
|
3
|
+
import { resolveRuntimeEnv } from "./server/lib/runtimeEnv.js";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
registerSurfaceRequestConstraint,
|
|
8
|
+
resolveRuntimeProfileFromSurface,
|
|
9
|
+
tryCreateProviderRuntimeFromApp
|
|
10
|
+
} from "@jskit-ai/kernel/server/platform";
|
|
11
|
+
import { matchesPathPrefix, normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
12
|
+
import { surfaceRuntime } from "./server/lib/surfaceRuntime.js";
|
|
13
|
+
|
|
14
|
+
const SPA_INDEX_FILE = "index.html";
|
|
15
|
+
const API_BASE_PATH = "/api";
|
|
16
|
+
const STATIC_GLOBAL_UI_PATHS = Object.freeze([
|
|
17
|
+
"/assets",
|
|
18
|
+
"/favicon.svg",
|
|
19
|
+
"/favicon.ico",
|
|
20
|
+
"/robots.txt",
|
|
21
|
+
"/manifest.webmanifest"
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function toRequestPathname(urlValue) {
|
|
25
|
+
const rawUrl = String(urlValue || "").trim() || "/";
|
|
26
|
+
try {
|
|
27
|
+
return normalizePathname(new URL(rawUrl, "http://localhost").pathname || "/");
|
|
28
|
+
} catch {
|
|
29
|
+
return normalizePathname(rawUrl.split("?")[0] || "/");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isApiPath(pathname) {
|
|
34
|
+
return matchesPathPrefix(pathname, API_BASE_PATH);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasFileExtension(pathname) {
|
|
38
|
+
return path.extname(normalizePathname(pathname)) !== "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveGlobalUiPaths(runtimeGlobalUiPaths = []) {
|
|
42
|
+
const paths = new Set(Array.isArray(runtimeGlobalUiPaths) ? runtimeGlobalUiPaths : []);
|
|
43
|
+
for (const staticPath of STATIC_GLOBAL_UI_PATHS) {
|
|
44
|
+
paths.add(staticPath);
|
|
45
|
+
}
|
|
46
|
+
return [...paths];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveStaticFilePath(pathname) {
|
|
50
|
+
const normalizedPathname = normalizePathname(pathname);
|
|
51
|
+
|
|
52
|
+
const relativePath = normalizedPathname.replace(/^\/+/, "");
|
|
53
|
+
if (!relativePath || relativePath.endsWith("/")) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const normalizedRelativePath = path.posix.normalize(relativePath);
|
|
58
|
+
if (
|
|
59
|
+
!normalizedRelativePath ||
|
|
60
|
+
normalizedRelativePath === "." ||
|
|
61
|
+
normalizedRelativePath === ".." ||
|
|
62
|
+
normalizedRelativePath.startsWith("../") ||
|
|
63
|
+
normalizedRelativePath.includes("/../")
|
|
64
|
+
) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalizedRelativePath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canServeStaticFile(distRoot, relativePath) {
|
|
72
|
+
if (!distRoot || !relativePath) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedDistRoot = path.resolve(distRoot);
|
|
77
|
+
const resolvedPath = path.resolve(normalizedDistRoot, relativePath);
|
|
78
|
+
if (!(resolvedPath === normalizedDistRoot || resolvedPath.startsWith(`${normalizedDistRoot}${path.sep}`))) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return existsSync(resolvedPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function createServer() {
|
|
86
|
+
const app = Fastify({
|
|
87
|
+
logger: true,
|
|
88
|
+
ajv: {
|
|
89
|
+
customOptions: {
|
|
90
|
+
allowUnionTypes: true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.get("/api/health", async () => {
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
app: "__APP_NAME__"
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const runtimeEnv = resolveRuntimeEnv();
|
|
102
|
+
const appRoot = path.resolve(process.cwd());
|
|
103
|
+
const distRoot = path.resolve(appRoot, "dist");
|
|
104
|
+
const hasWebBuild = existsSync(path.resolve(distRoot, SPA_INDEX_FILE));
|
|
105
|
+
const spaDocument = hasWebBuild ? readFileSync(path.resolve(distRoot, SPA_INDEX_FILE), "utf8") : "";
|
|
106
|
+
const runtime = await tryCreateProviderRuntimeFromApp({
|
|
107
|
+
appRoot,
|
|
108
|
+
profile: resolveRuntimeProfileFromSurface({
|
|
109
|
+
surfaceRuntime,
|
|
110
|
+
serverSurface: runtimeEnv.SERVER_SURFACE
|
|
111
|
+
}),
|
|
112
|
+
env: runtimeEnv,
|
|
113
|
+
logger: app.log,
|
|
114
|
+
fastify: app
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
registerSurfaceRequestConstraint({
|
|
118
|
+
fastify: app,
|
|
119
|
+
surfaceRuntime,
|
|
120
|
+
serverSurface: runtimeEnv.SERVER_SURFACE,
|
|
121
|
+
globalUiPaths: resolveGlobalUiPaths(runtime?.globalUiPaths || [])
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (hasWebBuild) {
|
|
125
|
+
await app.register(fastifyStatic, {
|
|
126
|
+
root: distRoot,
|
|
127
|
+
index: false,
|
|
128
|
+
serve: false
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
app.log.warn("Frontend build not found (dist/index.html). Page routes will return 404 until `npm run build`.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
app.setNotFoundHandler(async (request, reply) => {
|
|
135
|
+
const pathname = toRequestPathname(request?.url);
|
|
136
|
+
const method = String(request?.method || "GET")
|
|
137
|
+
.trim()
|
|
138
|
+
.toUpperCase();
|
|
139
|
+
if (isApiPath(pathname) || (method !== "GET" && method !== "HEAD")) {
|
|
140
|
+
return reply.code(404).send({
|
|
141
|
+
message: `Route ${method}:${pathname} not found`,
|
|
142
|
+
error: "Not Found",
|
|
143
|
+
statusCode: 404
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (hasFileExtension(pathname)) {
|
|
147
|
+
const staticFilePath = resolveStaticFilePath(pathname);
|
|
148
|
+
if (hasWebBuild && staticFilePath && canServeStaticFile(distRoot, staticFilePath)) {
|
|
149
|
+
return reply.sendFile(staticFilePath);
|
|
150
|
+
}
|
|
151
|
+
return reply.code(404).send({
|
|
152
|
+
message: `Route ${method}:${pathname} not found`,
|
|
153
|
+
error: "Not Found",
|
|
154
|
+
statusCode: 404
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (!hasWebBuild) {
|
|
158
|
+
return reply.code(404).send({
|
|
159
|
+
error: "Frontend build is not available. Run `npm run build`."
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return reply.type("text/html; charset=utf-8").send(spaDocument);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (runtime) {
|
|
166
|
+
app.log.info(
|
|
167
|
+
{
|
|
168
|
+
routeCount: runtime.routeCount,
|
|
169
|
+
surface: surfaceRuntime.normalizeSurfaceMode(runtimeEnv.SERVER_SURFACE),
|
|
170
|
+
providerPackages: runtime.providerPackageOrder,
|
|
171
|
+
packageOrder: runtime.packageOrder
|
|
172
|
+
},
|
|
173
|
+
"Registered JSKIT provider server runtime."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return app;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function startServer(options = {}) {
|
|
181
|
+
const runtimeEnv = resolveRuntimeEnv();
|
|
182
|
+
const port = Number(options?.port) || runtimeEnv.PORT;
|
|
183
|
+
const host = String(options?.host || "").trim() || runtimeEnv.HOST;
|
|
184
|
+
const app = await createServer();
|
|
185
|
+
await app.listen({ port, host });
|
|
186
|
+
return app;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { createServer, startServer };
|