@olenbetong/appframe-vite 6.1.0 → 6.1.2
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 +22 -1
- package/lib/generateTypes.js +24 -4
- package/lib/localization.js +7 -0
- package/lib/utils.d.ts +20 -0
- package/lib/utils.js +51 -0
- package/package.json +2 -2
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
|
-
|
|
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",
|
package/lib/generateTypes.js
CHANGED
|
@@ -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
|
-
|
|
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;`)
|
package/lib/localization.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "6.1.2",
|
|
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.
|
|
34
|
+
"@olenbetong/appframe-data": "1.5.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/jsdom": "^27.0.0",
|