@jskit-ai/create-app 0.1.26 → 0.1.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/create-app",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Scaffold minimal JSKIT app shells.",
5
5
  "type": "module",
6
6
  "files": [
@@ -53,7 +53,7 @@ function buildInitialBundleCommands(initialBundles) {
53
53
 
54
54
  const commands = [];
55
55
  if (normalizedPreset === "auth") {
56
- commands.push("npx jskit add auth-base --no-install");
56
+ commands.push("npx jskit add auth-base");
57
57
  }
58
58
 
59
59
  return commands;
@@ -634,7 +634,7 @@ export async function runCli(
634
634
  } else {
635
635
  stdout.write("First of all run npm install.:\n");
636
636
  stdout.write("Then add framework capabilities:\n");
637
- stdout.write("- npx jskit add auth-base --no-install\n");
637
+ stdout.write("- npx jskit add auth-base\n");
638
638
  stdout.write("- npx jskit list\n");
639
639
  stdout.write("Run server and client to see it in action:\n");
640
640
  stdout.write("- npm run dev\n");
@@ -15,6 +15,12 @@ Refresh JSKIT dependencies to the latest published versions:
15
15
  npm run jskit:update
16
16
  ```
17
17
 
18
+ Automate update + PR + merge release flow:
19
+
20
+ ```bash
21
+ npm run release
22
+ ```
23
+
18
24
  ## Server
19
25
 
20
26
  ```bash
@@ -29,5 +35,5 @@ App configuration files:
29
35
  ## Add Capabilities
30
36
 
31
37
  ```bash
32
- npx jskit add auth-base --no-install
38
+ npx jskit add auth-base
33
39
  ```
@@ -27,6 +27,7 @@
27
27
  "test": "node --test",
28
28
  "test:client": "vitest run tests/client",
29
29
  "verify": "npm run lint && npm run test && npm run test:client && npm run build && npx jskit doctor",
30
+ "release": "bash ./scripts/release.sh",
30
31
  "jskit:update": "bash ./scripts/update-jskit-packages.sh"
31
32
  },
32
33
  "dependencies": {
@@ -1,54 +1,8 @@
1
- import { access, constants as fsConstants } from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
-
5
- async function fileExists(absolutePath) {
6
- try {
7
- await access(absolutePath, fsConstants.F_OK);
8
- return true;
9
- } catch {
10
- return false;
11
- }
12
- }
13
-
14
- async function resolveAppRootFrom(moduleDirectory) {
15
- let currentDirectory = path.resolve(moduleDirectory);
16
-
17
- while (true) {
18
- const candidateConfigPath = path.join(currentDirectory, "config", "public.js");
19
- if (await fileExists(candidateConfigPath)) {
20
- return currentDirectory;
21
- }
22
-
23
- const parentDirectory = path.dirname(currentDirectory);
24
- if (parentDirectory === currentDirectory) {
25
- throw new Error("Unable to locate app root (missing config/public.js).");
26
- }
27
- currentDirectory = parentDirectory;
28
- }
29
- }
30
-
31
- async function loadConfigModule(absolutePath) {
32
- if (!(await fileExists(absolutePath))) {
33
- return {};
34
- }
35
-
36
- const loadedModule = await import(pathToFileURL(absolutePath).href);
37
- const loadedConfig = loadedModule?.config;
38
- return loadedConfig && typeof loadedConfig === "object" && !Array.isArray(loadedConfig) ? loadedConfig : {};
39
- }
1
+ import { loadAppConfigFromModuleUrl } from "@jskit-ai/kernel/server/support";
40
2
 
41
3
  async function loadAppConfig({ moduleUrl = import.meta.url } = {}) {
42
- const moduleDirectory = path.dirname(fileURLToPath(moduleUrl));
43
- const appRoot = await resolveAppRootFrom(moduleDirectory);
44
- const [publicConfig, serverConfig] = await Promise.all([
45
- loadConfigModule(path.join(appRoot, "config", "public.js")),
46
- loadConfigModule(path.join(appRoot, "config", "server.js"))
47
- ]);
48
-
49
- return Object.freeze({
50
- ...publicConfig,
51
- ...serverConfig
4
+ return loadAppConfigFromModuleUrl({
5
+ moduleUrl
52
6
  });
53
7
  }
54
8
 
@@ -87,8 +87,7 @@ for (const parentDirectory of parentDirectories) {
87
87
 
88
88
  const packageRoot = path.join(parentDirectory, entry.name);
89
89
  const packageJsonPath = path.join(packageRoot, "package.json");
90
- const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
91
- if (!fs.existsSync(packageJsonPath) || !fs.existsSync(descriptorPath)) {
90
+ if (!fs.existsSync(packageJsonPath)) {
92
91
  continue;
93
92
  }
94
93
 
@@ -120,6 +119,68 @@ for (const [packageDirName, packageRoot] of packageMap.entries()) {
120
119
  NODE
121
120
  }
122
121
 
122
+ link_package_bin_entries() {
123
+ local package_dir_name="$1"
124
+ local source_dir="$2"
125
+
126
+ node - "$APP_ROOT" "$package_dir_name" "$source_dir" <<'NODE'
127
+ const fs = require("node:fs");
128
+ const path = require("node:path");
129
+
130
+ const appRoot = process.argv[2];
131
+ const packageDirName = process.argv[3];
132
+ const sourceDir = process.argv[4];
133
+
134
+ const packageJsonPath = path.join(sourceDir, "package.json");
135
+ if (!fs.existsSync(packageJsonPath)) {
136
+ process.exit(0);
137
+ }
138
+
139
+ let packageJson = {};
140
+ try {
141
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
142
+ } catch {
143
+ process.exit(0);
144
+ }
145
+
146
+ const rawBin = packageJson?.bin;
147
+ let binEntries = [];
148
+ if (typeof rawBin === "string") {
149
+ binEntries = [[packageDirName, rawBin]];
150
+ } else if (rawBin && typeof rawBin === "object" && !Array.isArray(rawBin)) {
151
+ binEntries = Object.entries(rawBin);
152
+ }
153
+
154
+ if (binEntries.length < 1) {
155
+ process.exit(0);
156
+ }
157
+
158
+ const binDir = path.join(appRoot, "node_modules", ".bin");
159
+ const packageRoot = path.join(appRoot, "node_modules", "@jskit-ai", packageDirName);
160
+ fs.mkdirSync(binDir, { recursive: true });
161
+
162
+ for (const [rawBinName, rawBinTarget] of binEntries) {
163
+ const binName = String(rawBinName || "").trim();
164
+ const binTarget = String(rawBinTarget || "").trim();
165
+ if (!binName || !binTarget) {
166
+ continue;
167
+ }
168
+
169
+ const absoluteTarget = path.join(packageRoot, binTarget);
170
+ if (!fs.existsSync(absoluteTarget)) {
171
+ continue;
172
+ }
173
+
174
+ const binPath = path.join(binDir, binName);
175
+ fs.rmSync(binPath, { force: true, recursive: true });
176
+
177
+ const relativeTarget = path.relative(binDir, absoluteTarget) || absoluteTarget;
178
+ fs.symlinkSync(relativeTarget, binPath);
179
+ process.stdout.write(`[link-local] linked bin ${binName} -> ${relativeTarget}\n`);
180
+ }
181
+ NODE
182
+ }
183
+
123
184
  linked_count=0
124
185
 
125
186
  mkdir -p "$SCOPE_DIR"
@@ -132,6 +193,7 @@ while IFS=$'\t' read -r package_dir_name source_dir; do
132
193
  rm -rf "$target_path"
133
194
  ln -s "$source_dir" "$target_path"
134
195
  echo "[link-local] linked @jskit-ai/$package_dir_name -> $source_dir"
196
+ link_package_bin_entries "$package_dir_name" "$source_dir"
135
197
  linked_count=$((linked_count + 1))
136
198
  done < <(discover_local_package_map)
137
199
 
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ require_command() {
7
+ local name="$1"
8
+ if ! command -v "$name" >/dev/null 2>&1; then
9
+ echo "[release] required command not found: $name" >&2
10
+ exit 1
11
+ fi
12
+ }
13
+
14
+ ensure_clean_worktree() {
15
+ if [[ -n "$(git status --porcelain)" ]]; then
16
+ echo "[release] worktree is not clean. Commit/stash first." >&2
17
+ exit 1
18
+ fi
19
+ }
20
+
21
+ ensure_on_main() {
22
+ local current_branch
23
+ current_branch="$(git rev-parse --abbrev-ref HEAD)"
24
+ if [[ "$current_branch" != "main" ]]; then
25
+ echo "[release] current branch is \"$current_branch\". Switch to main first." >&2
26
+ exit 1
27
+ fi
28
+ }
29
+
30
+ main() {
31
+ require_command git
32
+ require_command npm
33
+ require_command gh
34
+
35
+ cd "$APP_ROOT"
36
+
37
+ if ! gh auth status >/dev/null 2>&1; then
38
+ echo "[release] gh is not authenticated. Run: gh auth login" >&2
39
+ exit 1
40
+ fi
41
+
42
+ ensure_clean_worktree
43
+ ensure_on_main
44
+
45
+ echo "[release] syncing local main..."
46
+ git fetch origin main
47
+ git pull --ff-only origin main
48
+
49
+ echo "[release] running package refresh..."
50
+ npm run jskit:update
51
+
52
+ if [[ -z "$(git status --porcelain)" ]]; then
53
+ echo "[release] no changes produced by jskit:update. Nothing to release."
54
+ exit 0
55
+ fi
56
+
57
+ local timestamp
58
+ local pretty_time
59
+ local branch
60
+ local commit_message
61
+ local pr_title
62
+ local pr_url
63
+
64
+ timestamp="$(date -u +%Y%m%d-%H%M%S)"
65
+ pretty_time="$(date -u +'%Y-%m-%d %H:%M UTC')"
66
+ branch="release/${timestamp}"
67
+ commit_message="chore: release ${pretty_time}"
68
+ pr_title="Release ${pretty_time}"
69
+
70
+ echo "[release] creating branch ${branch}..."
71
+ git checkout -b "$branch"
72
+
73
+ git add -A
74
+ git commit -m "$commit_message"
75
+ git push -u origin "$branch"
76
+
77
+ pr_url="$(
78
+ gh pr create \
79
+ --base main \
80
+ --head "$branch" \
81
+ --title "$pr_title" \
82
+ --body "Automated release commit generated by \`npm run release\`."
83
+ )"
84
+
85
+ echo "[release] created PR: $pr_url"
86
+ gh pr merge "$pr_url" --merge --delete-branch
87
+
88
+ echo "[release] merged. syncing local main..."
89
+ git checkout main
90
+ git pull --ff-only origin main
91
+
92
+ echo "[release] done."
93
+ }
94
+
95
+ main "$@"
@@ -113,7 +113,7 @@ if [[ "$dry_run_enabled" != "true" ]]; then
113
113
  echo "[jskit:update] generating managed migrations for changed packages."
114
114
  (
115
115
  cd "$APP_ROOT"
116
- npx jskit migrations changed --no-install
116
+ npx jskit migrations changed
117
117
  )
118
118
  fi
119
119
 
@@ -10,9 +10,11 @@ import {
10
10
  resolveRuntimeProfileFromSurface,
11
11
  tryCreateProviderRuntimeFromApp
12
12
  } from "@jskit-ai/kernel/server/platform";
13
+ import { matchesPathPrefix, normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
13
14
  import { surfaceRuntime } from "./server/lib/surfaceRuntime.js";
14
15
 
15
16
  const SPA_INDEX_FILE = "index.html";
17
+ const API_BASE_PATH = "/api";
16
18
  const STATIC_GLOBAL_UI_PATHS = Object.freeze([
17
19
  "/assets",
18
20
  "/favicon.svg",
@@ -24,19 +26,18 @@ const STATIC_GLOBAL_UI_PATHS = Object.freeze([
24
26
  function toRequestPathname(urlValue) {
25
27
  const rawUrl = String(urlValue || "").trim() || "/";
26
28
  try {
27
- return new URL(rawUrl, "http://localhost").pathname || "/";
29
+ return normalizePathname(new URL(rawUrl, "http://localhost").pathname || "/");
28
30
  } catch {
29
- return rawUrl.split("?")[0] || "/";
31
+ return normalizePathname(rawUrl.split("?")[0] || "/");
30
32
  }
31
33
  }
32
34
 
33
35
  function isApiPath(pathname) {
34
- const normalizedPathname = String(pathname || "").trim() || "/";
35
- return normalizedPathname === "/api" || normalizedPathname.startsWith("/api/");
36
+ return matchesPathPrefix(pathname, API_BASE_PATH);
36
37
  }
37
38
 
38
39
  function hasFileExtension(pathname) {
39
- return path.extname(String(pathname || "").trim()) !== "";
40
+ return path.extname(normalizePathname(pathname)) !== "";
40
41
  }
41
42
 
42
43
  function resolveGlobalUiPaths(runtimeGlobalUiPaths = []) {
@@ -48,10 +49,7 @@ function resolveGlobalUiPaths(runtimeGlobalUiPaths = []) {
48
49
  }
49
50
 
50
51
  function resolveStaticFilePath(pathname) {
51
- const normalizedPathname = String(pathname || "").trim() || "/";
52
- if (!normalizedPathname.startsWith("/")) {
53
- return "";
54
- }
52
+ const normalizedPathname = normalizePathname(pathname);
55
53
 
56
54
  const relativePath = normalizedPathname.replace(/^\/+/, "");
57
55
  if (!relativePath || relativePath.endsWith("/")) {