@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.
Files changed (60) hide show
  1. package/package.json +2 -2
  2. package/src/server/index.js +8 -1
  3. package/templates/base-shell/.jskit/lock.json +229 -0
  4. package/templates/base-shell/package.json +4 -2
  5. package/templates/base-shell/packages/main/src/client/providers/MainClientProvider.js +8 -0
  6. package/templates/base-shell/src/App.vue +7 -9
  7. package/templates/base-shell/src/components/ShellLayout.vue +12 -0
  8. package/templates/base-shell/src/components/menus/MenuLinkItem.vue +30 -0
  9. package/templates/base-shell/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  10. package/templates/base-shell/src/components/menus/TabLinkItem.vue +42 -0
  11. package/templates/base-shell/src/error.js +19 -0
  12. package/templates/base-shell/src/pages/home/index.vue +85 -21
  13. package/templates/base-shell/src/pages/home/settings/general/index.vue +40 -0
  14. package/templates/base-shell/src/pages/home/settings/index.vue +7 -0
  15. package/templates/base-shell/src/pages/home/settings.vue +109 -0
  16. package/templates/base-shell/src/pages/home.vue +8 -1
  17. package/templates/base-shell/src/placement.js +56 -0
  18. package/templates/base-shell/src/placementTopology.js +149 -0
  19. package/templates/base-shell/tests/e2e/adaptive-shell.spec.ts +4 -0
  20. package/templates/base-shell/tests/e2e/base-shell.spec.ts +1 -1
  21. package/templates/base-shell/vite.config.mjs +14 -0
  22. package/templates/minimal-shell/.github/workflows/verify.yml +29 -0
  23. package/templates/minimal-shell/.jskit/lock.json +31 -0
  24. package/templates/minimal-shell/AGENTS.md +10 -0
  25. package/templates/minimal-shell/Procfile +2 -0
  26. package/templates/minimal-shell/app.json +14 -0
  27. package/templates/minimal-shell/bin/server.js +8 -0
  28. package/templates/minimal-shell/config/public.js +41 -0
  29. package/templates/minimal-shell/config/server.js +1 -0
  30. package/templates/minimal-shell/config/surfaceAccessPolicies.js +3 -0
  31. package/templates/minimal-shell/eslint.config.mjs +19 -0
  32. package/templates/minimal-shell/favicon.svg +7 -0
  33. package/templates/minimal-shell/gitignore +8 -0
  34. package/templates/minimal-shell/index.html +13 -0
  35. package/templates/minimal-shell/jsconfig.json +8 -0
  36. package/templates/minimal-shell/package.json +55 -0
  37. package/templates/minimal-shell/packages/main/package.descriptor.mjs +69 -0
  38. package/templates/minimal-shell/packages/main/package.json +12 -0
  39. package/templates/minimal-shell/packages/main/src/client/index.js +13 -0
  40. package/templates/minimal-shell/packages/main/src/client/providers/MainClientProvider.js +20 -0
  41. package/templates/minimal-shell/packages/main/src/server/MainServiceProvider.js +20 -0
  42. package/templates/minimal-shell/packages/main/src/server/index.js +1 -0
  43. package/templates/minimal-shell/packages/main/src/server/loadAppConfig.js +9 -0
  44. package/templates/minimal-shell/packages/main/src/shared/index.js +10 -0
  45. package/templates/minimal-shell/packages/main/src/shared/schemas/index.js +22 -0
  46. package/templates/minimal-shell/scripts/dokku-set-remote-envs-via-ssh.sh +210 -0
  47. package/templates/minimal-shell/server/lib/runtimeEnv.js +45 -0
  48. package/templates/minimal-shell/server/lib/surfaceRuntime.js +10 -0
  49. package/templates/minimal-shell/server.js +189 -0
  50. package/templates/minimal-shell/src/App.vue +13 -0
  51. package/templates/minimal-shell/src/main.js +85 -0
  52. package/templates/minimal-shell/src/pages/home/index.vue +48 -0
  53. package/templates/minimal-shell/src/pages/home.vue +13 -0
  54. package/templates/minimal-shell/src/views/NotFound.vue +13 -0
  55. package/templates/minimal-shell/tests/client/smoke.vitest.js +7 -0
  56. package/templates/minimal-shell/tests/e2e/base-shell.spec.ts +57 -0
  57. package/templates/minimal-shell/tests/server/minimalShell.validator.test.js +131 -0
  58. package/templates/minimal-shell/tests/server/smoke.test.js +16 -0
  59. package/templates/minimal-shell/vite.config.mjs +84 -0
  60. 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,9 @@
1
+ import { loadAppConfigFromModuleUrl } from "@jskit-ai/kernel/server/support";
2
+
3
+ async function loadAppConfig({ moduleUrl = import.meta.url } = {}) {
4
+ return loadAppConfigFromModuleUrl({
5
+ moduleUrl
6
+ });
7
+ }
8
+
9
+ export { loadAppConfig };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared entrypoint for code used by both client and server.
3
+ *
4
+ * Keep this for genuinely app-local shared glue only.
5
+ *
6
+ * Example:
7
+ * export * from "./schemas/index.js";
8
+ * export * from "./constants/index.js";
9
+ */
10
+ export {};
@@ -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 };
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <v-app>
3
+ <v-main>
4
+ <v-container class="py-10 py-md-14">
5
+ <v-row justify="center">
6
+ <v-col cols="12" sm="11" md="10" lg="8" xl="7">
7
+ <RouterView />
8
+ </v-col>
9
+ </v-row>
10
+ </v-container>
11
+ </v-main>
12
+ </v-app>
13
+ </template>