@olenbetong/appframe-vite 6.1.0 → 6.1.1

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/lib/devServer.js CHANGED
@@ -6,6 +6,7 @@ import dotenv from "dotenv";
6
6
  import { JSDOM } from "jsdom";
7
7
  import { importJson } from "./importJson.js";
8
8
  import { getStringCache } from "./localization.js";
9
+ import { diagnoseServerResponse } from "./utils.js";
9
10
  dotenv.config({ quiet: true });
10
11
  let appPkg = await importJson("./package.json", true);
11
12
  let { appframe } = appPkg;
@@ -129,7 +130,27 @@ async function getArticleHtml() {
129
130
  let { hostname, username, password } = await getLoginInfo();
130
131
  let article = appframe.article?.id;
131
132
  let client = new Client(hostname);
132
- await client.login(username, password);
133
+ try {
134
+ await client.login(username, password);
135
+ }
136
+ catch (loginError) {
137
+ const isJsonError = loginError instanceof SyntaxError &&
138
+ (loginError.message.includes("not valid JSON") || loginError.message.includes("Unexpected token"));
139
+ if (isJsonError) {
140
+ let loginBody = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true&RequireTwoFactor=0`;
141
+ let diagnostic = await diagnoseServerResponse(`https://${hostname}/login`, {
142
+ method: "POST",
143
+ body: loginBody,
144
+ headers: {
145
+ "Content-Type": "application/x-www-form-urlencoded",
146
+ Accept: "application/json",
147
+ },
148
+ followRedirects: true,
149
+ });
150
+ throw new Error(`login to ${hostname} (article HTML fetch): server returned non-JSON. Server response:\n${diagnostic}`);
151
+ }
152
+ throw loginError;
153
+ }
133
154
  let response = await client.fetch(`/${article}`, {
134
155
  timeout: 30_000,
135
156
  method: "GET",
@@ -5,7 +5,7 @@ import { resolve } from "node:path";
5
5
  import vm from "node:vm";
6
6
  import { Client, DataObject, generateApiDataHandler, getDefaultClient, Procedure, setDefaultClient, } from "@olenbetong/appframe-data";
7
7
  import { importJson } from "./importJson.js";
8
- import { createLogMessage } from "./utils.js";
8
+ import { createLogMessage, diagnoseServerResponse, wrapJsonError } from "./utils.js";
9
9
  // ─── Type generation logic ────────────────────────────────────────────────────
10
10
  function afTypeToTsType(type, isProc = false) {
11
11
  if (type === "uniqueidentifier")
@@ -136,7 +136,27 @@ const RETRY_DELAY_BASE_MS = 2_000;
136
136
  async function doGenerateTypes(hostname, username, password, appframe) {
137
137
  const client = new Client(hostname);
138
138
  setDefaultClient(client);
139
- await withTimeout(client.login(username, password), TIMEOUT_MS, "login");
139
+ try {
140
+ await withTimeout(client.login(username, password), TIMEOUT_MS, "login");
141
+ }
142
+ catch (loginError) {
143
+ const isJsonError = loginError instanceof SyntaxError &&
144
+ (loginError.message.includes("not valid JSON") || loginError.message.includes("Unexpected token"));
145
+ if (isJsonError) {
146
+ let loginBody = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true&RequireTwoFactor=0`;
147
+ let diagnostic = await diagnoseServerResponse(`https://${hostname}/login`, {
148
+ method: "POST",
149
+ body: loginBody,
150
+ headers: {
151
+ "Content-Type": "application/x-www-form-urlencoded",
152
+ Accept: "application/json",
153
+ },
154
+ followRedirects: true,
155
+ });
156
+ throw new Error(`login to ${hostname}: server returned non-JSON. Server response:\n${diagnostic}`);
157
+ }
158
+ throw loginError;
159
+ }
140
160
  const articleId = appframe.article?.id ?? appframe.article;
141
161
  const articleHost = appframe.article?.hostname ?? hostname;
142
162
  const scriptGlobal = {
@@ -194,10 +214,10 @@ async function doGenerateTypes(hostname, username, password, appframe) {
194
214
  types = headerComment + types;
195
215
  // Append article feature flags
196
216
  const dsFeatures = createArticlesFeaturesDataHandler();
197
- const features = await withTimeout(dsFeatures.retrieve({
217
+ const features = await wrapJsonError(`fetch article features for '${articleId}' on ${articleHost}`, () => withTimeout(dsFeatures.retrieve({
198
218
  maxRecords: -1,
199
219
  whereClause: `[HostName] = '${articleHost}' AND [ArticleId] = '${articleId}'`,
200
- }), TIMEOUT_MS, "fetch article features");
220
+ }), TIMEOUT_MS, "fetch article features"));
201
221
  if (features.length) {
202
222
  types += `\n\ndeclare module "@olenbetong/appframe-core" {\n\tinterface AfArticle {\n\t\tfeatures: {\n\t\t\t${features
203
223
  .map((f) => `/**\n\t\t\t * ${f.Name}: ${f.Description}\n\t\t\t */\n\t\t\t${f.Key}: boolean;`)
@@ -67,6 +67,13 @@ export const localizeMiddleware = async (req, res, _next) => {
67
67
  "X-Requested-With": "XMLHttpRequest",
68
68
  },
69
69
  });
70
+ const contentType = result.headers.get("content-type") ?? "";
71
+ if (!result.ok || !contentType.includes("application/json")) {
72
+ let body = await result.text();
73
+ throw new Error(`POST ${uri} returned ${result.status} ${result.statusText} ` +
74
+ `(Content-Type: ${contentType || "none"}). ` +
75
+ `Response snippet: ${body.slice(0, 300)}`);
76
+ }
70
77
  let data = await result.json();
71
78
  addStringToCache(text, data);
72
79
  res.statusCode = 200;
package/lib/utils.d.ts CHANGED
@@ -5,6 +5,26 @@ export declare const PLUGIN_TAG: string;
5
5
  /** Returns a colored environment tag (`[dev]` / `[stage]` / `[prod]`) based on hostname. */
6
6
  export declare function getServerPrefix(hostname: string): string;
7
7
  export declare function getServerName(hostname: string): string;
8
+ /**
9
+ * Wraps an async operation. If the caught error is a JSON parse error
10
+ * (SyntaxError from undici/fetch `.json()` on an HTML response), re-throws
11
+ * with `context` prepended so it's obvious which network call failed.
12
+ */
13
+ export declare function wrapJsonError<T>(context: string, fn: () => Promise<T>): Promise<T>;
14
+ /**
15
+ * Makes a diagnostic HTTP request and returns the full response body + metadata
16
+ * as a human-readable string. Used to show what the server is actually returning
17
+ * when a JSON-parse error occurs inside a library that swallows the response body.
18
+ *
19
+ * Pass followRedirects: true to follow the redirect chain (useful when the library
20
+ * auto-follows redirects and fails on the final response).
21
+ */
22
+ export declare function diagnoseServerResponse(url: string, options?: {
23
+ method?: string;
24
+ body?: string;
25
+ headers?: Record<string, string>;
26
+ followRedirects?: boolean;
27
+ }): Promise<string>;
8
28
  export interface LogMessageOptions {
9
29
  /** Pre-formatted chalk tag, e.g. from `getServerPrefix()` or `PLUGIN_TAG`. */
10
30
  tag?: string;
package/lib/utils.js CHANGED
@@ -40,6 +40,57 @@ export function getServerName(hostname) {
40
40
  }
41
41
  return hostname;
42
42
  }
43
+ /**
44
+ * Wraps an async operation. If the caught error is a JSON parse error
45
+ * (SyntaxError from undici/fetch `.json()` on an HTML response), re-throws
46
+ * with `context` prepended so it's obvious which network call failed.
47
+ */
48
+ export async function wrapJsonError(context, fn) {
49
+ try {
50
+ return await fn();
51
+ }
52
+ catch (error) {
53
+ if (error instanceof SyntaxError &&
54
+ (error.message.includes("not valid JSON") || error.message.includes("Unexpected token"))) {
55
+ throw new Error(`${context}: server returned non-JSON — likely an HTML redirect or error page. ` +
56
+ `Original parse error: ${error.message}`);
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ /**
62
+ * Makes a diagnostic HTTP request and returns the full response body + metadata
63
+ * as a human-readable string. Used to show what the server is actually returning
64
+ * when a JSON-parse error occurs inside a library that swallows the response body.
65
+ *
66
+ * Pass followRedirects: true to follow the redirect chain (useful when the library
67
+ * auto-follows redirects and fails on the final response).
68
+ */
69
+ export async function diagnoseServerResponse(url, options = {}) {
70
+ const method = options.method ?? "GET";
71
+ const redirect = options.followRedirects ? "follow" : "manual";
72
+ try {
73
+ const res = await fetch(url, {
74
+ method,
75
+ headers: { Accept: "text/html,application/json,*/*", ...options.headers },
76
+ body: options.body,
77
+ redirect,
78
+ });
79
+ const contentType = res.headers.get("content-type") ?? "(none)";
80
+ const location = res.headers.get("location");
81
+ const body = await res.text();
82
+ let info = `${method} ${url} → ${res.status} ${res.statusText} | Content-Type: ${contentType}`;
83
+ if (location)
84
+ info += ` | Location: ${location}`;
85
+ if (res.redirected)
86
+ info += ` | Final URL: ${res.url}`;
87
+ info += `\n--- Response body ---\n${body}\n--- End of body ---`;
88
+ return info;
89
+ }
90
+ catch (diagError) {
91
+ return `${method} ${url} failed: ${diagError?.message ?? diagError}`;
92
+ }
93
+ }
43
94
  /**
44
95
  * Assembles a consistently-formatted log message:
45
96
  * `HH:mm:ss [tag] (source) message`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "6.1.0",
3
+ "version": "6.1.1",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "dotenv": "^17.2.3",
32
32
  "jsdom": "29.0.2",
33
33
  "rollup-plugin-visualizer": "^6.0.5",
34
- "@olenbetong/appframe-data": "1.4.1"
34
+ "@olenbetong/appframe-data": "1.4.2"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/jsdom": "^27.0.0",